Reputation: 8631
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
Reputation: 8655
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:
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.)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 stringName
is at least X characters longName
does not contain punctuation or whitespaceType
is a valid valueLike 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.
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:
None
(None = 0
, or just None
implicitly assigned to 0
)Admin
implicitly assigned to 0
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:
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?
2
for a Basic user, but it will get a validation error for an invalid Type value1
for a Superuser, but it will save a Basic user1
for a Basic user and think it has a Superuser2
and will never show Basic user functionalityWhat 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:
3
for Basic users still works2
for a Superuser will get a validation error for an invalid value2
and will never show Superuser functionalityThe 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.
Upvotes: 4
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.
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
.
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
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
Reputation: 9495
✔️ 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
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
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
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
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
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
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
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
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
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
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
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
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