Skip to content
Go back

C# 枚举完全指南:声明、赋值与最佳实践

C# 枚举完全指南:声明、赋值与最佳实践

代码里散落的魔法数字是一种慢性毒药。六个月后读到 if (role == 3) 时,你必须翻历史记录才能想起 3 代表什么。在 C# 里使用 enum 就能彻底消除这个问题——它用有意义的名字替换掉数字字面量,让代码读起来像散文一样自然。

这篇文章覆盖枚举的完整用法:声明方式、显式赋值、与整数之间的相互转换、比较与迭代,以及把枚举用好所需要的最佳实践。

声明枚举

声明一个枚举只需一行样板代码,后面跟上你的领域所需要的成员:

public enum OrderStatus
{
    Pending,
    Processing,
    Shipped,
    Delivered,
    Cancelled
}

public 修饰符让这个枚举在整个项目中都可以访问。也可以用 internal(程序集范围)、private(嵌套在类内部)或 protected(基类内部)。

枚举成员默认从 0 开始自动编号:Pending0Processing1,依此类推。当这些值只在代码内部使用时,隐式编号没有问题。但如果值会持久化到数据库或与外部系统共享,就应该显式赋值——下一节会讲到。

声明位置建议:枚举是类型,应该放在命名空间级别(和使用它的类型同文件,或者单独一个文件)。不要把枚举声明在方法内部,那样会不必要地限制它的可见性。

显式赋值

需要精确控制每个成员对应哪个整数时,可以显式赋值:

public enum HttpStatusCode
{
    Ok                  = 200,
    Created             = 201,
    NoContent           = 204,
    BadRequest          = 400,
    Unauthorized        = 401,
    Forbidden           = 403,
    NotFound            = 404,
    InternalServerError = 500
}

以下场景必须使用显式值:

有一条关键原则:持久化枚举绝不能依赖隐式顺序。假设你有 enum Role { Viewer, Editor, Admin },后来在 EditorAdmin 之间插入了 Moderator,所有存储的 Admin 值就会静默变成 Moderator,编译器不会给出任何警告。

使用枚举变量

声明之后,使用 C# 枚举非常直接:

// 声明并赋值
OrderStatus status = OrderStatus.Pending;

// 比较
if (status == OrderStatus.Pending)
{
    Console.WriteLine("Order has not started yet.");
}

// 作为类字段
public class Order
{
    public OrderStatus Status { get; set; } = OrderStatus.Pending;
}

// 传给方法
void ProcessOrder(Order order, OrderStatus targetStatus)
{
    if (order.Status != targetStatus)
    {
        // ...
    }
}

编译器会强制你只能使用命名成员——OrderStatus.Pending,而不是 5。这种类型安全能消除一整类 bug。

枚举与 int 的相互转换

有时需要把枚举转成底层整数(用于存储或与外部数据比较),或者把整数解析回枚举:

OrderStatus status = OrderStatus.Shipped;

// 枚举转 int——需要显式强制转换
int statusInt = (int)status;               // 2

// int 转枚举——同样需要显式强制转换
OrderStatus fromInt = (OrderStatus)2;      // Shipped

// 危险:C# 不会验证转换的合法性
OrderStatus invalid = (OrderStatus)99;     // 不会抛异常!值 99 没有对应的名称
Console.WriteLine(invalid);               // "99"——不是成员名

int 转回枚举不会抛出异常,即使值未定义。因此,在转换之前要始终验证外部整数:

int incoming = GetStatusFromExternalApi();

if (!Enum.IsDefined(typeof(OrderStatus), incoming))
{
    throw new ArgumentException($"Unknown order status: {incoming}");
}

OrderStatus status = (OrderStatus)incoming;

如果输入来自 API 或用户表单的字符串,可以用 Enum.TryParse

if (Enum.TryParse<OrderStatus>(userInput, ignoreCase: true, out OrderStatus parsed))
{
    // 安全使用 parsed
}
else
{
    // 处理无效输入
}

比较枚举值

C# 枚举成员支持相等、不等和关系比较:

OrderStatus a = OrderStatus.Shipped;
OrderStatus b = OrderStatus.Delivered;

bool equal  = (a == b);   // false
bool notEq  = (a != b);   // true

// 关系比较(基于底层 int 值)
bool shipped = (a < b);   // true——Shipped (2) < Delivered (3)

关系比较对于有天然顺序的枚举(如严重级别或优先级)有意义,对于没有内在顺序的枚举则意义不大。相等比较是最常见的用法。

对于基于枚举的分支逻辑,switch 表达式是更简洁、更彻底的写法:

string GetLabel(OrderStatus status) => status switch
{
    OrderStatus.Pending    => "Pending",
    OrderStatus.Processing => "Processing",
    OrderStatus.Shipped    => "Shipped",
    OrderStatus.Delivered  => "Delivered",
    OrderStatus.Cancelled  => "Cancelled",
    _                      => "Unknown"
};

遍历所有枚举值

两种写法能覆盖大多数遍历需求:

// .NET 5+ 推荐写法
foreach (OrderStatus status in Enum.GetValues<OrderStatus>())
{
    Console.WriteLine($"{status} = {(int)status}");
}
// 输出:
// Pending = 0
// Processing = 1
// Shipped = 2
// Delivered = 3
// Cancelled = 4

// 获取所有成员名称
string[] names = Enum.GetNames<OrderStatus>();

// 早期 .NET Framework / .NET Core 的旧写法
foreach (OrderStatus status in (OrderStatus[])Enum.GetValues(typeof(OrderStatus)))
{
    // ...
}

枚举遍历在这些场景中很实用:

在属性和方法参数中使用枚举

枚举让 API 自文档化。对比下面两个方法签名:

// 不好——int status 是什么意思?
void UpdateOrder(int orderId, int status) { }

// 好——枚举让意图显而易见
void UpdateOrder(int orderId, OrderStatus status) { }

第二个版本在类型层面说清楚了契约,调用者不需要查文档就知道哪些值是合法的。

在领域模型里,枚举属性可以搭配可空类型处理可选值:

public class Shipment
{
    public Guid Id { get; init; }
    public OrderStatus Status { get; set; } = OrderStatus.Pending;
    public DateTimeOffset? ShippedAt { get; set; }
    public DateTimeOffset? DeliveredAt { get; set; }
}

常见错误

没有定义零值成员。 未初始化的枚举字段默认值是 0。如果没有成员映射到 0,你就会持有一个没有名字的合法变量:

// 有问题——default(OrderStatus) 是 0,但没有成员 = 0
public enum OrderStatus
{
    Pending = 1,
    Shipped = 2
}

// 安全——0 有明确对应的成员
public enum OrderStatus
{
    None    = 0,
    Pending = 1,
    Shipped = 2
}

不验证就转换整数。 如上所述,(OrderStatus)99 能编译和运行,不会报错。对外部数据做转换前必须验证。

添加成员但没有更新所有 switch 语句。 如果给 OrderStatus 加了 OnHold,每个没有对应分支的 switch 都会静默跳过该值。在 discard arm 里加上抛异常的处理,能让这类遗漏在运行时暴露出来。

用枚举表示开放式分类。 如果新值可能在运行时出现(用户自定义标签、插件类型名),应该用 string 或值对象。枚举适合那些在编译时就确定的、封闭的已知集合。

最佳实践速查

参考


Tags


Next

用 C# 写 .NET 垃圾回收器(第八部分):内部指针与砖表