枚举(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 检测某一位。