Acaz Souza
Acaz Souza

Reputation: 8631

Should an Enum start with a 0 or a 1?

Imagine I have defined the following Enum:

public enum Status : byte
{
    Inactive = 1,
    Active = 2,
}

What's the best practice to use enum? Should it start with 1 like the above example or start with 0 (without the explicit values) like this:

public enum Status : byte
{
    Inactive,
    Active
}

Upvotes: 159

Views: 115327

Answers (17)

JamesFaix
JamesFaix

Reputation: 8655

Assign all values starting at 1 and use `Nullable{T}` to represent no value, unless you can't

I appreciate Microsoft's framework guidelines, but I disagree with them on enum practices. I think there is a lot of subtlety in the various use cases of enums that aren't really addressed there, or in other answers here.

However, flag enums really do need a None = 0 value to work. The rest of this answer does not apply to flag enums.

Also, before going on, it might be good to state the golden rule of C# enums:

Enums are a semi-type-safe minefield

For this answer, I will use this hypothetical enum:

enum UserType {
  Basic,
  Admin
}

There are different ways we might use this enum type.


Case 1: Part of a data structure queried from the DB

class UserQueryResult {
  // Name of the saved user
  public string Name { get; set; }

  // Type of the saved user
  public UserType Type { get; set; }

  // Lucky number of the user, if they have one
  public int? LuckyNumber { get; set; }
}

Case 2: Part of a search query

class UserSearchQuery {
  // If set, only return users with this name
  public string Name { get; set; }

  // If set, only return users with this type
  public UserType Type { get; set; }

  // If set, only return users with this lucky number
  public int? LuckyNumber { get; set; }
}

Case 3: Part of a POST request

class CreateUserRequest {
  // Name of user to save
  public string Name { get; set; }

  // Type of user to save
  public UserType Type { get; set; }

  // Lucky number of user, if they have one
  public int? LuckyNumber { get; set; }
}

These three classes all look the same, but the data comes from different places and is validated and processed differently.

Case 1: Part of a data structure queried from the DB

We can make some assumptions about the validity of this data, because it should have been validated before saving.

  • Name should be a valid non-empty string.
  • Type should be either Basic or Admin, never null or some other invalid value. (For now, ignore how this property is persisted, whether as INT/VARCHAR/etc.)
  • Nulls are never valid for Name or Type. If using newer C# language features, the Name property might be declared as non-nullable (string! Name), although this might not be directly supported by all ORMs, and so you may need to validate against nulls after querying data.

Case 2: Part of a search query

This is a client request, so there may be invalid input. Additionally, these properties should be optional, so clients can search using only the filters they care about.

You might want to model this type using Nullable<T> for value types and explicit nullable reference types.

public class UserSearchQuery {
  // Only return users with this name
  public string? Name { get; set; }

  // Only return users with this type
  public UserType? Type { get; set; }

  // If set, only return users with this lucky number
  public int? LuckyNumber { get; set; }
}

Things you may want to validate:

  • Name is either null or a non-empty string. Alternately, you may just treat empty or whitespace as null. (You probably don't want to validate the value is a real user name. If its not valid, the search will return 0 results.)
  • Type is a valid enum value, or some representation of "no filter". For example, if a client sends Type = "Superuser" this may indicate a client bug and a 400 response would be helpful.

Case 3: Part of a POST request

This is also client input, but these properties should not allow null/blank values, and there will be different validation rules.

Things you may want to validate:

  • Name is a non-null, non-empty string
  • Name is at least X characters long
  • Name does not contain punctuation or whitespace
  • Type is a valid value

Like case 1, you may want to use string! Name to more accurately represent your data. However, if this is being parsed from HTTP requests, you may need to explicitly validate against nulls still, depending on the framework you are using.


So, what is the best way to represent "no type"?

The framework guidelines say that we should add an element to our enum to represent this:

enum UserType {
  None,
  Basic,
  Admin
}

So how does this affect our 3 use cases? It affects all of them, because they are all using UserType.

Case 1: Part of a data structure queried from the DB

Instances of UserQueryResult can now be created with Type = UserType.None.

Of course, this isn't the first invalid state our typing allows. UserQueryResult already allowed Name = "", but we are adding a possible invalid state.

In places where we access UserQueryResult.Type, we should already have a defensive way to handle invalid UserType values, since the type system allows things like (UserType)999.

Case 2: Part of a search query

If we stick with using Nullable<T> for value types on our optional properties, we now have two ways to represent "do not filter on UserType".

  • Type = UserType.None
  • Type = null

This means anywhere we use this type we need some && or || logic to deal with both cases.

If we get rid of Nullable<T> on enum types but leave it on other value types, then we reduce the number of options, but have a more complicated API contract with multiple conventions for us and clients to remember.

Case 3: Part of a POST request

The types now allow Type = UserType.None on this request. We'll need to add a special validation rule to check against this.


What we can see from the effects of this change on these 3 cases is that we have coupled the list of valid values to the representation of "no value". "No value" is only valid for Case 2, but we have forced the code for Case 1 and Case 3 to handle extra "no value" complexity.

Additionally, we can see in Case 2 that we already have a generic way to represent "no value" for value types, which is Nullable<T>. In many ways this resembles the null handling for reference types, bringing us close to a single unified way to represent "no value" across all types, reducing developer mental load.

Conclusion 1

Use Nullable<T> for "no value", for consistency, and so that you have a distinct type to represent "a value that is never 'no value'".


So that is why you shouldn't add a None value. But why should you explicitly assign enum int values?

Reason 1: Unassigned properties have the value default(T)

For reference types, default(T) == null. For value types, default(T) == (T)0.

Let's say a client wants to POST a request to create a new user. A good JSON payload would look like this:

{
  "Name": "James",
  "Type": "Admin",
  "LuckyNumber": 12
}

(For readability, our JSON parser is configured to accept strings for enums. Whether using strings or ints for enums in JSON is not really relevant here.)

As expected, this payload will be parsed to a C# object like this:

{
  Name = "James",
  Type = UserType.Admin,
  LuckyNumber = 12
}

What happens if our client sends incomplete JSON?

{
  "Name": "James",
  // Client forgot to add Type property
  "LuckyNumber": 12
}

This will be parsed as

{
  Name = "James",
  Type = default(UserType),
  LuckyNumber = 12
}

Again, default(UserType) == (UserType)0.

Our enum could be declared in one of three ways:

  1. Start with None (None = 0, or just None implicitly assigned to 0)
  2. Start with Admin implicitly assigned to 0
  3. Start with Admin = 1

In case 1, Type gets parsed as None. Since None is part of our enum, we already need to validate against this case to prevent saving None to the DB. However, I already covered the reasons why you shouldn't have a None value.

In case 2, Type gets parsed as Admin. After that happens, there isn't a way to differentiate between an Admin value that came from "Type": "Admin" in the payload, vs Type missing in the payload. This is obviously not good.

In case 3, Type gets parsed as (UserType)0, which doesn't have a name. This looks odd at first, but is actually the best possible scenario. Because enums allow invalid values (like (UserType)999), we should be validating against invalid values from clients anyway. This just makes "unassigned" an invalid value instead of a valid one.

To me, case 3 also seems well aligned with the recent additions to C# that make it harder to represent invalid values: non-nullable reference types and required properties. Conversely, case 1 feels like a legacy pattern from C# 1, before generics and Nullable<T>.

Reason 2: Avoid accidental contract changes

If your enum's integer values are part of the external-facing contract of your service, changing the integers can break clients.

There are two main places enums are external-facing:

  • Saving/loading from a database
  • HTTP requests/responses

Here is the easiest way to accidentally create a breaking change with enums. Start with this enum:

enum UserType {
  Admin,     // = 0
  Superuser, // = 1
  Basic      // = 2
}

Clients are using hardcoded values 0, 1, and 2 for user types. Then the business wants to deprecate the Superuser type. A dev removes that enum element.

enum UserType {
  Admin,     // = 0
  Basic      // = 1
}

How many behaviors are now broken?

  • A client might POST 2 for a Basic user, but it will get a validation error for an invalid Type value
  • A client might POST 1 for a Superuser, but it will save a Basic user
  • A client might GET 1 for a Basic user and think it has a Superuser
  • A client will never GET 2 and will never show Basic user functionality

What if we had assigned explicit values to the enum fields at the beginning, and then removed Superuser?

enum UserType {
  Admin = 1
  // No more Superuser = 2
  Basic = 3
}

No accidental breakage:

  • POSTing and GETing 3 for Basic users still works
  • POSTing 2 for a Superuser will get a validation error for an invalid value
  • A client will never GET 2 and will never show Superuser functionality

The HTTP case can also be mitigated by serializing enums as strings instead of numbers. However, that isn't ideal if you really need to minimize payload sizes. String enum serialization is less common on the DB side, I think because the same team often owns the DB and the service using it, whereas API clients can be more distributed and communication can be more challenging.

Conclusion 2:

Always explicitly assign values to each enum field, to prevent accidental breaking changes. But never assign `0` (except for flag enums), so that you can differentiate between unassigned properties and valid values.

Upvotes: 4

Zhi Yuan
Zhi Yuan

Reputation: 194

Prefer setting the first Enum member's value to 1 if the Enum does not have a concept of default value for the following reasons.

Intuition

C# sets the Enum to 0 by default. So, unless that first Enum member is really a default value, it's intuitive to not have it mapped to 0.

Allows Enforcing of Required Enums for Web API

Consider the following Minimal Web API:

using Microsoft.AspNetCore.Mvc;
using MiniValidation; // See https://github.com/dotnet/aspnetcore/issues/39063
using System.ComponentModel.DataAnnotations;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Returns true if validation is successful, false otherwise
app.MapGet("/", ([FromBody] MyClass myClass) => MiniValidator.TryValidate(myClass, out _));

app.Run();

class MyClass 
{ 
    [EnumDataType(typeof(MyEnum))] // Validates `MyEnum` is a valid enum value
    public MyEnum MyEnum { get; set; } 
}

enum MyEnum { One, Two }

Suppose that it's mandatory for the client to supply a value for MyEnum; sending an empty JSON string {} results in the endpoint returning false.

However, the above implementation returns true; Model Validation passes because C# defaults MyEnum to 0, which is mapped to MyEnum.One.

By modifying the Enum to enum MyEnum { One = 1, Two }, the endpoint returns false; Model Validation fails because none of the Enum's members are mapped to 0.


Caveat

Enum's guidelines documentation state

DO provide a value of zero on simple enums.

But it doesn't seem that violating this guideline leads to negative consequences.

Upvotes: 1

Madhusha Ravishani
Madhusha Ravishani

Reputation: 41

if the enum starts with zero then no need to assign integer values.It starts with 0 & increment by 1. Inactive = 0, Active = 1.

 public enum Status : byte{
   Inactive,
   Active
}

If you want to assign specific value for 1st one, you need to assign the value for it.Here, Inactive = 1, Active = 0.

 public enum Status : byte{
    Inactive =1,
    Active =0
 }

Upvotes: 0

Andrey Taptunov
Andrey Taptunov

Reputation: 9495

Framework Design Guidelines:

✔️ DO provide a value of zero on simple enums.

Consider calling the value something like "None." If such a value is not appropriate for this particular enum, the most common default value for the enum should be assigned the underlying value of zero.

Framework Design Guidelines / Designing Flag Enums:

❌ AVOID using flag enum values of zero unless the value represents "all flags are cleared" and is named appropriately, as prescribed by the next guideline.

✔️ DO name the zero value of flag enums None. For a flag enum, the value must always mean "all flags are cleared."

Upvotes: 190

dru
dru

Reputation: 81

If not specified numbering starts at 0.

It is important to be explicit since enums are often serialized and stored as an int, not a string.

For any enum stored in the database, we always explicitly number the options to prevent shifting and reassignment during maintenance.

According to Microsoft, the recommended convention is use the first zero option to represent an uninitialized or the most common default value.

Below is a shortcut to start numbering at 1 instead of 0.

public enum Status : byte
{
    Inactive = 1,
    Active
}

If you wish to set flag values in order to use bit operators on enum values, don't start numbering at the zero value.

Upvotes: 7

zzzzBov
zzzzBov

Reputation: 179086

Unless you have a good reason to use the raw values, you should only ever be using implicit values and referencing them with Status.Active and Status.Inactive.

The catch is that you might want to store data in a flat file or DB, or use a flat file or DB that someone else created. If you're making it yourself, make it so the numbering fits what the Enum is used for.

If the data is not yours, of course you're going to want to use whatever the original dev had used as a numbering scheme.

If you're planning on using the Enum as a set of flags, there is a simple convention that's worth following:

enum Example
{
  None      = 0,            //  0
  Alpha     = 1 << 0,       //  1
  Beta      = 1 << 1,       //  2
  Gamma     = 1 << 2,       //  4
  Delta     = 1 << 3,       //  8
  Epsilon   = 1 << 4,       // 16
  All       = ~0,           // -1
  AlphaBeta = Alpha | Beta, //  3
}

Values should be powers of two and can be expressed using bit-shift operations. None, obviously should be 0, but All is less obviously -1. ~0 is the binary negation of 0 and results in a number that has every bit set to 1, which represents a value of -1. For compound flags (often used for convenience) other values may be merged using the bitwise or operator |.

Upvotes: 8

wprl
wprl

Reputation: 25407

Don't start them at 0 unless there's a reason to, such as using them as indices to an array or list, or if there's some other practical reason (like using them in bitwise operations).

Your enum should start exactly where it needs to. It needn't be sequential, either. The values, if they are explicitly set, need to reflect some semantic meaning or practical consideration. For example, an enum of "bottles on the wall" should be numbered from 1 to 99, while an enum for powers of 4 should probably start at 4 and continue with 16, 64, 256, etc.

Furthermore, adding a zero-valued element to the enum should only be done if it represents a valid state. Sometimes "none," "unknown," "missing," etc. are valid values, but many times they are not.

Upvotes: 0

FishBasketGordo
FishBasketGordo

Reputation: 23142

Unless you have a specific reason to change it, leave enums with their default values, which begin at zero.

public enum Status : byte
{
    Inactive,
    Active
}

Upvotes: 15

Jonathan Cline IEEE
Jonathan Cline IEEE

Reputation: 705

If you start at 1, then you can easily get a count of your things.

{
    BOX_THING1     = 1,
    BOX_THING2     = 2,
    BOX_NUM_THING  = BOX_THING2
};

If you start at 0, then use the first one as a value for uninitialized things.

{
    BOX_NO_THING   = 0,
    BOX_THING1     = 1,
    BOX_THING2     = 2,
    BOX_NUM_THING  = BOX_THING2
};

Upvotes: 1

to StackOverflow
to StackOverflow

Reputation: 124716

An Enum is a value type and its default value (for example for an Enum field in a class) will be 0 if not initialized explicitly.

Therefore you generally want to have 0 as an defined constant (e.g. Unknown).

In your example, if you want Inactive to be the default, then it should have the value zero. Otherwise you might want to consider adding a constant Unknown.

Some people have recommended that you don't explicitly specify values for your constants. Probably good advice in most cases, but there are some cases when you will want to do so:

  • Flags enums

  • Enums whose values are used in interop with external systems (e.g. COM).

Upvotes: 17

Michael Sagalovich
Michael Sagalovich

Reputation: 2549

I would say, it depends on how you use them. For flagging enum it is a good practice to have 0 for None value, like that:

[Flags]
enum MyEnum
{
    None = 0,
    Option1 = 1,
    Option2 = 2,
    Option3 = 4,
    All = Option1 | Option2 | Option3,
}

When your enum is likely to be mapped to a database lookup table, I'd start it with 1. It should not matter much for professionally written code, but this improves readability.

In other cases I'd leave it as it is, giving no care whether they start with 0 or 1.

Upvotes: 7

Justin Niessner
Justin Niessner

Reputation: 245429

First of all, unless you're specifying specific values for a reason (the numeric value has meaning somewhere else, i.e. The Database or external service) then don't specify numeric values at all and let them be explicit.

Second of all, you should always have a zero value item (in non-flags enums). That element will be used as the default value.

Upvotes: 0

pstrjds
pstrjds

Reputation: 17428

Well, I guess I stand in disagreement with most answers that say not to explicitly number them. I always explicitly number them, but that is because in most cases I end up persisting them in a data stream where they are stored as an integer value. If you don't explicitly add the values and then add a new value you can break the serialization and then not be able to accurately load old persisted objects. If you are going to do any type of persistent store of these values then I would highly recommend explicitly setting the values.

Upvotes: 83

Tomas McGuinness
Tomas McGuinness

Reputation: 7691

I like to start my enums at 0, since that's the default, but I also like to include a Unknown value, with a value of -1. This then becomes the default and can help with debugging sometimes.

Upvotes: -1

Hooch
Hooch

Reputation: 29683

Don't assign any numbers. Just use it like it supposed to be used.

Upvotes: 2

Mark Schultheiss
Mark Schultheiss

Reputation: 34168

I would start a boolean type enum with a 0.

Unless "Inative" means something other than "Inactive" :)

This retains the standard for those.

Upvotes: 5

John Humphreys
John Humphreys

Reputation: 39294

I'd say best practice is to not number them and let it be implicit - which would start from 0. Since its implicit its the language preference which is always good to follow :)

Upvotes: 5

Related Questions