枚举应该以0还是1开头?

154

假设我已经定义了以下枚举:

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

如何最好地使用枚举类型?是像上面的例子一样从 1 开始,还是不带显式值从 0 开始,例如:

public enum Status : byte
{
    Inactive,
    Active
}

14
你是否真的需要明确地给它们编号? - Yuck
169
枚举类型的创建正是为了使类似这样的问题不再重要。 - BoltClock
9
@Daniel -- 哎呀!如果你觉得布尔值可以解决问题,最好使用枚举类型,而不是在考虑枚举类型时使用布尔值。 - AAT
22
由于FileNotFound的取值,当然。 - Joubarc
7
http://xkcd.com/163/ 这个漫画比它适用于数组索引更适用于枚举类型。 - leftaroundabout
显示剩余7条评论
17个回答

2
不要分配任何数字。 只需按照预期使用即可。

1

从1开始分配所有值,并使用`Nullable{T}`表示没有值,除非不能

我赞赏微软的框架指南,但在枚举实践方面我持有不同意见。我认为枚举在各种用例中存在很多微妙之处,这些问题在那里或其他答案中并没有得到很好的解决。

然而,标志枚举确实需要一个None = 0的值才能正常工作。本回答中的其余内容不适用于标志枚举。

此外,在继续之前,可能有必要说明C#枚举的黄金法则:

枚举是一个半类型安全的雷区

在本回答中,我将使用以下假设的枚举:

enum UserType {
  Basic,
  Admin
}

我们可以以不同的方式使用这个枚举类型。
案例1:从数据库查询的数据结构的一部分
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; }
}

案例2:搜索查询的一部分
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; }
}

案例3:POST请求的一部分
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; }
}

这三个类看起来都一样,但数据来自不同的地方,并且经过不同的验证和处理。
案例1:从数据库查询的数据结构的一部分
我们可以对这些数据的有效性做出一些假设,因为在保存之前应该已经进行了验证。
- Name 应该是一个有效的非空字符串。 - Type 应该是 Basic 或 Admin,永远不会是 null 或其他无效值。(暂时忽略此属性如何持久化,是作为 INT/VARCHAR 等等。) - Name 或 Type 永远不允许为空。如果使用较新版本的 C# 语言特性,Name 属性可能被声明为非可空(string! Name),尽管这可能并不是所有 ORM 直接支持的,所以在查询数据后可能需要针对空值进行验证。
案例2:搜索查询的一部分
这是客户端请求,因此可能存在无效输入。此外,这些属性应该是可选的,因此客户端可以仅使用他们关心的筛选器进行搜索。
你可能想要使用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 是一个有效的值
与案例1类似,您可能希望使用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契约变得更加复杂,需要记住多个约定。
第三种情况:作为POST请求的一部分
现在,这个请求允许Type = UserType.None。我们需要添加一个特殊的验证规则来检查这个情况。
从这个改变对这三种情况的影响中,我们可以看到的是,我们将有效值列表与“无值”的表示耦合在一起。对于Case 2来说,“无值”是有效的,但我们却迫使Case 1和Case 3的代码处理额外的“无值”复杂性。
此外,在Case 2中,我们已经有了一种通用的方式来表示值类型的“无值”,即Nullable。在许多方面,这类似于引用类型的null处理,使我们接近一种统一的方式来表示所有类型的“无值”,减轻开发人员的心理负担。
结论1
为了保持一致性,并且使您有一个明确的类型来表示“永远不是‘无值’的值”,请使用Nullable来表示“无值”。
所以这就是为什么你不应该添加一个None值。但是为什么你应该明确地分配枚举int值呢?
原因1:未分配的属性具有default(T)的值
对于引用类型,default(T) == null。 对于值类型,default(T) == (T)0
假设客户端想要发送一个POST请求来创建一个新用户。一个好的JSON负载应该像这样:
{
  "Name": "James",
  "Type": "Admin",
  "LuckyNumber": 12
}

(为了易读性,我们的JSON解析器被配置为接受字符串作为枚举类型。在JSON中使用字符串还是整数来表示枚举类型并不重要。)
(正如预期的那样,这个有效载荷将被解析为一个C#对象,如下所示:)
{
  Name = "James",
  Type = UserType.Admin,
  LuckyNumber = 12
}

如果我们的客户发送不完整的JSON会发生什么?
{
  "Name": "James",
  // Client forgot to add Type property
  "LuckyNumber": 12
}

这将被解析为

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

再说一遍,default(UserType) == (UserType)0
我们的枚举可以有三种声明方式:
  1. None开头(None = 0,或者隐式地赋值为0
  2. Admin隐式赋值为0
  3. Admin = 1开头
在情况1中,Type被解析为None。由于None是我们枚举的一部分,我们已经需要对这种情况进行验证,以防止将None保存到数据库中。然而,我已经解释了为什么你不应该有一个None值。
在情况2中,Type被解析为Admin。之后,就没有办法区分一个来自负载中的"Type": "Admin"Admin值和负载中缺少Type的情况。这显然是不好的。
在第3种情况下,Type被解析为(UserType)0,它没有名称。一开始看起来有点奇怪,但实际上这是最好的情况。因为枚举允许无效值(例如(UserType)999),我们应该始终对客户端提供的无效值进行验证。这只是将“未分配”变成了一个无效值,而不是有效值。
对我来说,第3种情况也与最近对C#进行的一些改进非常吻合,这些改进使得表示无效值更加困难:非可空引用类型和必需属性。相反,第1种情况感觉像是C# 1中的传统模式,在泛型和Nullable<T>之前。 原因2:避免意外的合同变更 如果您的枚举整数值是服务的外部接口合同的一部分,更改这些整数值可能会破坏客户端。
枚举作为外部接口的两个主要场景如下:
  • 从数据库保存/加载
  • HTTP请求/响应
以下是意外创建破坏性变更的最简单方法。从这个枚举开始:
enum UserType {
  Admin,     // = 0
  Superuser, // = 1
  Basic      // = 2
}

客户正在使用硬编码的值0、1和2来表示用户类型。然后业务部门希望废弃“超级用户”类型。开发人员删除了该枚举元素。
enum UserType {
  Admin,     // = 0
  Basic      // = 1
}

现在有多少行为被破坏了?

  • 对于普通用户,客户端可能会进行POST请求,并提供2,但是它将因为无效的类型值而收到验证错误
  • 对于超级用户,客户端可能会进行POST请求,并提供1,但是它会保存为普通用户
  • 对于普通用户,客户端可能会进行GET请求,并获取1,并认为它拥有超级用户权限
  • 客户端将永远不会进行GET请求,也不会显示普通用户功能

如果我们在开始时为枚举字段分配明确的值,然后移除Superuser,会怎么样?

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

不会发生意外的破坏:

  • 对于普通用户,POST和GET请求中的3仍然有效
  • 对于超级用户,POST请求中的2将因为无效值而得到验证错误
  • 客户端永远不会GET到2,也永远不会显示超级用户功能

HTTP情况也可以通过将枚举序列化为字符串而进行缓解,而不是数字。然而,如果您真的需要最小化负载大小,这并不理想。我认为,在数据库方面,较少使用字符串枚举序列化,可能是因为同一团队通常拥有数据库和使用它的服务,而API客户端可能更加分散,沟通可能更具挑战性。

结论2:


始终明确为每个枚举字段分配值,以防止意外的破坏性更改。但是,除了标志枚举之外,永远不要分配`0`,这样您就可以区分未分配的属性和有效值。

1

如果枚举没有默认值的概念,则最好将第一个枚举成员的值设置为1,原因如下:

直觉

C# 默认将枚举设置为0。因此,除非第一个枚举成员真的是默认值,否则将其映射到0不符合直觉。

允许强制执行 Web API 所需的 Enums

考虑以下最小 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

如果枚举以零开头,则无需分配整数值。它从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
首先,除非您有特定的原因(例如数字值在其他某个地方具有意义,例如数据库或外部服务)要指定特定值,否则根本不要指定数字值,让它们明确。
其次,在非标志枚举中,您应始终设置零值项。该元素将用作默认值。

0

除非有理由将它们用作数组或列表的索引,或者有其他实际原因(如在位运算中使用它们),否则不要从0开始。

您的enum应该从需要的地方开始。 它不必是连续的。 如果它们被明确设置,则这些值需要反映一些语义含义或实际考虑因素。 例如,“墙上的瓶子”的enum应从1到99编号,而4的幂的enum可能应该从4开始,并继续使用16、64、256等。

此外,仅当零值元素表示有效状态时,才应将其添加到enum中。 有时,“无”,“未知”,“缺失”等是有效值,但很多时候它们不是。


-1

我喜欢从0开始定义枚举,因为那是默认值,但我也喜欢包括一个未知值,其值为-1。这样它就成为默认值,并有时可以帮助调试。


4
糟糕的想法。作为值类型,枚举始终被初始化为零。如果你要有一些值表示未知或未初始化,它需要是0。你不能将默认值更改为-1,因为CLR中全局都硬编码了以零填充。 - Ben Voigt
啊,我没意识到。通常我在声明/初始化时设置枚举值/属性的值。感谢指点。 - Tomas McGuinness

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接