Menu

C# 枚举(enum):取值、ToString、Parse、Flags 与遍历

C# 中的枚举是怎么工作的:声明命名常量,底层整数值与类型转换,用 ToString 把枚举转成字符串、用 Parse 和 TryParse 把字符串转成枚举,列出所有值,[Flags] 与位运算符和 HasFlag,对枚举使用 switch,以及处理未定义的值。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

枚举(enumeration)是一种类型,它的值是一组固定的命名常量:订单状态、星期几、日志级别。每个名称底层都是一个整数,但类型系统会防止 OrderStatus 与普通的 int 或其他枚举混用。

声明和使用枚举

在花括号中列出成员名称。默认情况下,第一个是 0,之后每个依次加一:

输出:

Paid
On its way
True
2

枚举是一个真正的类型:接收 OrderStatus 的方法不会被误传 3 或 LogLevel。枚举是值类型,所以永远不会是 null,用 == 按值比较。

显式值与底层类型

你可以自己指定数字。只要这个数字会离开你的程序(数据库列、HTTP 状态码、文件格式),这一点就很重要,因为那时重新编号会破坏已存储的数据:

输出:

404
Created
418
1
Byte

有两点值得注意。把 int 转换为枚举永远不会失败:(HttpStatus)418 是一个合法的值,只是没有名称,打印出来就是数字。另外,底层类型可以是任何整数类型(byte、short、long 等),这只对在意存储空间的代码有意义;int 是默认值,几乎总是正确的选择。

以后添加成员时,要么加在末尾,要么给出显式值。在 Paid 和 Shipped 之间插入 Refunded,会悄无声息地改变它后面每个成员的数字。

枚举转字符串

ToString() 返回成员名称,Console.WriteLine 和字符串内插用的也是它。格式字符串可以改变输出:

输出:

Warning
2
00000002
[Warning]
Error
Error
Needs attention

成员名称是标识符,所以不能包含空格,也不会被翻译。显示给用户的文本,请像 Label 那样自己映射,或者用一个 Dictionary<LogLevel, string>。有些代码库会在每个成员上加 [Description("Needs attention")] 特性,再通过反射读取;反射与特性那一页介绍了这种查找是如何实现的。

字符串转枚举:Parse 与 TryParse

Enum.Parse 把名称转换回值,什么都匹配不到时抛出 ArgumentException。Enum.TryParse 则返回 false,对于任何你无法控制的输入,你要的都是它:

输出:

Large
Medium
Parse threw ArgumentException for Huge
small  parsed=True  value=Small  defined=True
XL     parsed=False value=Small  defined=True
2      parsed=True  value=Large  defined=True
7      parsed=True  value=7      defined=False

最后两行就是陷阱所在。两个方法都接受数字字符串,所以 "7" 能成功解析成一个没有名称的 Size。而 TryParse 失败时会把结果设为 0,在这里就是看起来完全合法的 Small。文本来自查询字符串、配置文件或表单时,一定要同时检查返回值和 Enum.IsDefined:

if (Enum.TryParse(input, true, out Size size) && Enum.IsDefined(typeof(Size), size))
{
    // safe to use size
}

.NET Core 2.0 及更高版本增加了泛型的 Enum.Parse<Size>("Large"),不需要强制转换。

列出所有值

Enum.GetValues 返回每个成员,按数值排序(按无符号数比较,所以负数成员排在最后);Enum.GetNames 返回它们的名称。填充下拉列表或对照所有选项做验证,用的就是它们:

输出:

Free       0 EUR/month
Starter    9 EUR/month
Pro       29 EUR/month
Team      99 EUR/month
Free | Starter | Pro | Team
3 paid plans

Enum.GetValues(typeof(Plan)) 返回一个普通的 Array,所以在使用 LINQ 之前要先 Cast<Plan>()。在 .NET 5 及更高版本中,Enum.GetValues<Plan>() 直接返回带类型的 Plan[]。

Flags:组合值

有些枚举描述的是一组选项,而不是单个选择:文件权限、商店营业的日子、通知渠道。给每个成员一个独立的位(1、2、4、8 等),加上 None = 0,并把枚举标记为 [Flags]。这样值就可以用 | 组合:

输出:

Read, Share
Editor, Share
True
False
Editor
3
Read, Delete
True

各运算符的作用:| 设置位,& ~X 清除位,^ 翻转位,(value & X) != 0 或 value.HasFlag(X) 检测位。HasFlag(X) 的意思是“X 的所有位都已设置”,所以 HasFlag(None) 对任何值都为 true,HasFlag(Editor) 则要求 Read 和 Write 都已设置。

注意第二行:当某个命名组合覆盖了一部分已设置的位时,ToString 会使用它,所以 Read | Write | Share 打印为 Editor, Share。如果要用 Enum.Parse 以外的方式解析 ToString 的输出,要记住这一点。

这个特性不会改变算术运算,它改变的是格式化:没有 [Flags] 时,Read | Share 打印为 9,因为没有哪个成员的值恰好是它。有了它,ToString 和 Parse 都能处理逗号分隔的形式。成员仍然必须是 2 的幂;如果用默认编号(0、1、2)写 Read, Write, Delete,Write | Delete 就等于 3,一个毫无意义的值。

对枚举使用 switch

switch 是根据枚举执行不同操作的自然方式。要加上 default 分支,因为枚举变量可能持有没有名称的值:

switch (status)
{
    case OrderStatus.Pending:
    case OrderStatus.Paid:
        return "Preparing";
    case OrderStatus.Shipped:
        return "On the way";
    case OrderStatus.Delivered:
        return "Delivered";
    default:
        return "Unknown";
}

从 C# 8 起,switch 表达式写起来更短。如果没有 _ 分支,编译器会发出警告:缺少某个命名成员时是 CS8509,所有名称都处理了、但 (OrderStatus)7 这样没有名称的值没有处理时是 CS8524:

string text = status switch
{
    OrderStatus.Pending or OrderStatus.Paid => "Preparing",   // 'or' pattern: C# 9
    OrderStatus.Shipped => "On the way",
    OrderStatus.Delivered => "Delivered",
    OrderStatus.Cancelled => "Cancelled",
    _ => throw new ArgumentOutOfRangeException(nameof(status)),
};

默认值与未定义的值

任何枚举的默认值都是 0,不管有没有成员的值是 0。字段、数组元素以及失败的 TryParse 都会产生它。设计时要考虑到这一点:

  • 让 0 成为一个有意义的“未设置”成员(None、Unknown),而不是一个真正的选项。否则,未初始化的字段会悄悄地被当成第一个真正的选项。
  • 用 Enum.IsDefined 验证来自外部的数字。对于 [Flags] 枚举,IsDefined 对没有名称的组合(Read | Share)返回 false,所以要改为检查位:用一个覆盖所有位的 All 成员,判断 (value & ~Permissions.All) == 0。

常见错误

  • 只相信 TryParse。 数字字符串也能解析成功,解析失败则得到 0。请加上 Enum.IsDefined。
  • 存储的值依赖隐式编号。 插入一个成员会让后面的成员重新编号。凡是要持久化的枚举,都给出显式值。
  • Flags 不用 2 的幂。 默认编号(0、1、2、3)的位会重叠。请用 1、2、4、8,或 1 << n。
  • 把 ToString() 显示给用户。 成员名称是代码标识符。请把值映射为显示文本。
  • switch 中没有 default。 枚举可能持有命名成员之外的值。

常见问题

C# 中如何把枚举转换为字符串?

调用 ToString():OrderStatus.Shipped.ToString() 返回 "Shipped",字符串内插也是同样的结果。ToString("D") 则给出数字。编译时就知道的名称,nameof(OrderStatus.Shipped) 是一个常量。面向用户、带空格或需要翻译的文本,请自己把值映射成字符串(用 switch 或字典),不要依赖成员名。

C# 中如何把字符串转换为枚举?

使用 Enum.TryParse<OrderStatus>(text, true, out var status),文本匹配不到任何成员时,它返回 false 而不是抛出异常(true 表示不区分大小写)。Enum.Parse(typeof(OrderStatus), text) 遇到错误输入会抛出 ArgumentException。两者都接受 "42" 这样的数字字符串,所以输入来自用户时,要用 Enum.IsDefined 检查结果。

C# 中如何在枚举和 int 之间转换?

两个方向都直接强制转换:int code = (int)OrderStatus.Paid; 和 var status = (OrderStatus)2;。从 int 转换永远不会失败,即使数字没有对应的成员;结果是一个打印出来是数字的枚举值。数字来自外部时,用 Enum.IsDefined(typeof(OrderStatus), value) 验证。

C# 中如何遍历枚举的所有值?

foreach (OrderStatus s in Enum.GetValues(typeof(OrderStatus))) 按数值顺序访问每个成员。从 .NET 5 起有了泛型版本 Enum.GetValues<OrderStatus>(),不需要强制转换。Enum.GetNames(typeof(OrderStatus)) 以字符串形式返回所有名称。

C# 枚举上的 [Flags] 有什么作用?

它标记一个枚举,表示其中的值是可以用 | 组合的位,比如 Read | Write。给每个成员一个 2 的幂(1、2、4、8),再加上 None = 0。这个特性让 ToString() 把组合打印成 "Read, Write",并让 Enum.Parse 能读回这种格式。用 HasFlag 或 (value & Permissions.Write) != 0 检测某一位。

Coddy 编程语言插图

和 Coddy 一起学编程

开始