假设我已经定义了以下枚举:
public enum Status : byte
{
Inactive = 1,
Active = 2,
}
如何最好地使用枚举类型?是像上面的例子一样从 1
开始,还是不带显式值从 0
开始,例如:
public enum Status : byte
{
Inactive,
Active
}
假设我已经定义了以下枚举:
public enum Status : byte
{
Inactive = 1,
Active = 2,
}
如何最好地使用枚举类型?是像上面的例子一样从 1
开始,还是不带显式值从 0
开始,例如:
public enum Status : byte
{
Inactive,
Active
}
我赞赏微软的框架指南,但在枚举实践方面我持有不同意见。我认为枚举在各种用例中存在很多微妙之处,这些问题在那里或其他答案中并没有得到很好的解决。
然而,标志枚举确实需要一个None = 0
的值才能正常工作。本回答中的其余内容不适用于标志枚举。
此外,在继续之前,可能有必要说明C#枚举的黄金法则:
在本回答中,我将使用以下假设的枚举:
enum UserType {
Basic,
Admin
}
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; }
}
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; }
}
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; }
}
Nullable<T>
来对值类型和显式可空引用类型进行建模。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; }
}
你可能想要验证的事项:
Name
要么是 null
或者是一个非空字符串。或者,你可以将空字符串或空格视为 null
。(你可能不想验证该值是否是一个真实的用户名。如果不是有效的用户名,搜索将返回0个结果。)Type
是一个有效的枚举值,或者表示"无过滤器"的某种形式。例如,如果客户端发送 Type = "Superuser"
,这可能表示客户端错误,应该返回400响应。情况3:POST请求的一部分
这也是客户端输入,但这些属性不应允许 null
或空白值,并且会有不同的验证规则。
你可能想要验证的事项:
Name
是一个非 null
、非空字符串Name
至少包含 X 个字符Name
不包含标点符号或空格Type
是一个有效的值string! Name
来更准确地表示您的数据。然而,如果这是从HTTP请求中解析出来的,根据您使用的框架,您可能仍然需要显式地验证null值。
那么,表示“无类型”的最佳方式是什么?
框架指南建议我们在枚举中添加一个元素来表示这个情况:
enum UserType {
None,
Basic,
Admin
}
那么这会对我们的三个用例产生什么影响?它会影响所有这些用例,因为它们都在使用UserType
。
用例1:从数据库查询的数据结构的一部分
现在可以使用Type = UserType.None
创建UserQueryResult
的实例。
当然,这不是我们的类型允许的第一个无效状态。 UserQueryResult
已经允许Name = ""
,但我们正在添加一个可能的无效状态。
在访问UserQueryResult.Type
的地方,我们应该已经有了一种防御性的处理方式来处理无效的UserType
值,因为类型系统允许类似(UserType)999
这样的操作。
用例2:搜索查询的一部分
如果我们继续在可选属性上使用Nullable<T>
作为值类型,我们现在有两种表示“不筛选UserType
”的方法。
Type = UserType.None
Type = null
&&
或||
逻辑来处理两种情况。Nullable<T>
,但将其保留在其他值类型上,则减少了选项的数量,但对于我们和客户端来说,API契约变得更加复杂,需要记住多个约定。Type = UserType.None
。我们需要添加一个特殊的验证规则来检查这个情况。
None
值。但是为什么你应该明确地分配枚举int
值呢?default(T)
的值default(T) == null
。
对于值类型,default(T) == (T)0
。{
"Name": "James",
"Type": "Admin",
"LuckyNumber": 12
}
{
Name = "James",
Type = UserType.Admin,
LuckyNumber = 12
}
{
"Name": "James",
// Client forgot to add Type property
"LuckyNumber": 12
}
这将被解析为
{
Name = "James",
Type = default(UserType),
LuckyNumber = 12
}
default(UserType) == (UserType)0
。None
开头(None = 0
,或者隐式地赋值为0
)Admin
隐式赋值为0
Admin = 1
开头Type
被解析为None
。由于None
是我们枚举的一部分,我们已经需要对这种情况进行验证,以防止将None
保存到数据库中。然而,我已经解释了为什么你不应该有一个None
值。Type
被解析为Admin
。之后,就没有办法区分一个来自负载中的"Type": "Admin"
的Admin
值和负载中缺少Type
的情况。这显然是不好的。Type
被解析为(UserType)0
,它没有名称。一开始看起来有点奇怪,但实际上这是最好的情况。因为枚举允许无效值(例如(UserType)999
),我们应该始终对客户端提供的无效值进行验证。这只是将“未分配”变成了一个无效值,而不是有效值。Nullable<T>
之前。
原因2:避免意外的合同变更
如果您的枚举整数值是服务的外部接口合同的一部分,更改这些整数值可能会破坏客户端。enum UserType {
Admin, // = 0
Superuser, // = 1
Basic // = 2
}
enum UserType {
Admin, // = 0
Basic // = 1
}
现在有多少行为被破坏了?
2
,但是它将因为无效的类型值而收到验证错误1
,但是它会保存为普通用户1
,并认为它拥有超级用户权限如果我们在开始时为枚举字段分配明确的值,然后移除Superuser
,会怎么样?
enum UserType {
Admin = 1
// No more Superuser = 2
Basic = 3
}
不会发生意外的破坏:
3
仍然有效2
将因为无效值而得到验证错误2
,也永远不会显示超级用户功能HTTP情况也可以通过将枚举序列化为字符串而进行缓解,而不是数字。然而,如果您真的需要最小化负载大小,这并不理想。我认为,在数据库方面,较少使用字符串枚举序列化,可能是因为同一团队通常拥有数据库和使用它的服务,而API客户端可能更加分散,沟通可能更具挑战性。
如果枚举没有默认值的概念,则最好将第一个枚举成员的值设置为1
,原因如下:
C# 默认将枚举设置为0
。因此,除非第一个枚举成员真的是默认值,否则将其映射到0
不符合直觉。
考虑以下最小 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 }
MyEnum
的值;发送空的JSON字符串{}
会导致终端返回false
。true
;模型验证通过,因为C#将MyEnum
默认为0
,这被映射到MyEnum.One
。enum MyEnum { One = 1, Two }
,终端返回false
;模型验证失败,因为枚举的成员都没有映射到0
。
注意事项
枚举指南文档中提到:
在简单枚举中,应该提供零值。
但是似乎违反这个指南并不会导致负面后果。
如果枚举以零开头,则无需分配整数值。它从0开始,每次增加1。Inactive = 0,Active = 1。
public enum Status : byte{
Inactive,
Active
}
如果你想为第一个变量分配特定的值,你需要为它赋值。这里,Inactive = 1,Active = 0。
public enum Status : byte{
Inactive =1,
Active =0
}
除非有理由将它们用作数组或列表的索引,或者有其他实际原因(如在位运算中使用它们),否则不要从0开始。
您的enum
应该从需要的地方开始。 它不必是连续的。 如果它们被明确设置,则这些值需要反映一些语义含义或实际考虑因素。 例如,“墙上的瓶子”的enum
应从1到99编号,而4的幂的enum
可能应该从4开始,并继续使用16、64、256等。
此外,仅当零值元素表示有效状态时,才应将其添加到enum
中。 有时,“无”,“未知”,“缺失”等是有效值,但很多时候它们不是。
我喜欢从0开始定义枚举,因为那是默认值,但我也喜欢包括一个未知值,其值为-1。这样它就成为默认值,并有时可以帮助调试。