
在 C# 代码里,enum 和常量(const / static readonly)都能替换魔法数字,让代码更可读。但它们解决的问题不一样,选错了会给维护带来真实的麻烦。这个选择会在领域建模、配置、API 设计和持久化等场景里反复出现。
本文给你一个具体的决策框架,讲清楚两者的根本差异、各自的适用条件,以及什么时候应该选第三条路——枚举类(Enumeration Class)。
根本区别
const 是单个独立的命名值:
public const int MaxRetries = 3;
public const string ApiVersion = "v2";
public const double TaxRate = 0.08;
常量之间没有类型层面的关联,编译器不会要求一个变量只持有某几个定义好的常量值。
enum 则是把一组相关常量收束在同一个类型下:
public enum OrderStatus
{
Pending,
Processing,
Shipped,
Delivered,
Cancelled
}
OrderStatus 类型本身表达了”这个变量只能是这几个值之一”。编译器在每个调用处都会执行这个约束。你不可能把 HttpStatusCode.Ok 传给要求 OrderStatus 的参数。
核心区分点:常量给单个值命名;enum 定义了一个有边界的领域类型。
适合用 enum 的场景
满足以下全部条件时,选 enum:
- 有效值的集合在编译期是封闭且有限的
- 值之间互斥(或者需要组合,此时加
[Flags]) - 变量只应持有这几个定义好的值之一
- 类型安全和穷举性 switch 检查对你有价值
典型例子:
// 星期 —— 封闭、有限、互斥
public enum DayOfWeek { Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday }
// 日志级别 —— 封闭、有序、互斥
public enum LogLevel { Trace, Debug, Information, Warning, Error, Critical }
// 支付方式 —— 设计时封闭,类型安全
public enum PaymentMethod { CreditCard, DebitCard, BankTransfer, Cryptocurrency }
这些场景都能从编译器拒绝非法赋值、以及 switch 的穷举性检查中受益。
此外,如果你需要遍历所有合法值(Enum.GetValues<T>()),或者需要按名称序列化/反序列化(JsonStringEnumConverter),enum 也是更自然的选择。
适合用常量的场景
满足以下任意条件时,选 const 或 static readonly:
- 是单个独立的配置值,和其他值没有类型关联
- 是
string、double等不能作为 enum 底层类型的值 - 表达的是阈值、限制或外部标识符,而不是领域选项
- 需要在编译期可用(特性参数、默认值、数组大小)
// 配置常量 —— 独立,没有类型关联
public static class AppConstants
{
public const int MaxRetries = 3;
public const int DefaultPageSize = 25;
public const string DefaultCulture = "en-US";
public const string ApiVersion = "v2";
}
// 阈值常量
public static class BusinessRules
{
public const decimal FreeShippingThreshold = 50.00m;
public const int OrderCancellationWindowHours = 24;
}
如果值是引用类型、需要在启动时计算,或者需要从配置中读取,用 static readonly 而不是 const:
public static class Defaults
{
// TimeSpan 不是编译期常量类型,不能用 const
public static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(30);
public static readonly Uri BaseApiUrl = new Uri("https://api.example.com");
}
需要注意的是,常量不提供类型安全保障。两个不相关的 int 常量具有相同的类型,这类错误编译器不会报:
void ProcessOrder(int maxItems, int retries) { }
// 参数顺序传反了,编译器不报错,但运行时是 bug
ProcessOrder(MaxRetries, DefaultPageSize);
enum 因为是不同类型,就不存在这类问题。
决策矩阵
| 考察维度 | const / static readonly | enum |
|---|---|---|
| 类型安全 | 否 | 是 |
| 穷举性 switch | 否 | 是(CS8509) |
| 遍历所有值 | 否 | Enum.GetValues<T>() |
| 扩展性 | 高 | 需要重新编译 |
| 字符串序列化 | 原生支持 | 需要 JsonStringEnumConverter |
| 每个值携带行为 | 否 | 否(此时用枚举类) |
| 编译期常量 | 是 | 仅成员本身(变量不是) |
经验法则:只要你会对这个值写 switch,就用 enum;如果它是单个阈值或标识符,就用常量。
第三个选项:枚举类
有时候 const 和 enum 都不够用。枚举类(Enumeration Class)模式——一个带有 static readonly 实例的密封类——兼具 enum 的封闭结构和普通类的能力:
public sealed class OrderStatus : IEquatable<OrderStatus>
{
public static readonly OrderStatus Pending = new("Pending", "等待处理");
public static readonly OrderStatus Processing = new("Processing", "处理中");
public static readonly OrderStatus Shipped = new("Shipped", "已发货");
public static readonly OrderStatus Delivered = new("Delivered", "已完成");
public static readonly OrderStatus Cancelled = new("Cancelled", "已取消");
public string Name { get; }
public string Description { get; }
private OrderStatus(string name, string description)
{
Name = name;
Description = description;
}
public override string ToString() => Name;
public bool Equals(OrderStatus? other) => other is not null && Name == other.Name;
public override bool Equals(object? obj) => Equals(obj as OrderStatus);
public override int GetHashCode() => Name.GetHashCode();
}
// 使用
var status = OrderStatus.Shipped;
Console.WriteLine(status); // "Shipped"
Console.WriteLine(status.Description); // "已发货"
当每个值需要关联数据或行为时——描述、URL、颜色码、验证规则、因值而异的方法——就用枚举类。
代价是:代码量更多,不支持开箱即用的穷举性 switch 检查,以整数形式持久化到数据库也更麻烦。
持久化时怎么选
存到数据库时,两种方式各有利弊:
按整数存储:快速、紧凑,但脆弱。如果在没有显式赋值的情况下调整了 enum 成员顺序,存储数据会悄无声息地映射到错误的名称。持久化时必须给每个成员显式赋值:
public enum OrderStatus
{
Pending = 1,
Processing = 2,
Shipped = 3,
Delivered = 4,
Cancelled = 5
}
按字符串存储:可读性好,调整顺序或新增成员都安全,但占用更多存储且查询稍慢。在 EF Core 中用 HasConversion<string>() 配置:
// EF Core model builder
entity.Property(o => o.Status)
.HasConversion<string>();
常量:直接映射到列类型,不需要额外转换。
在 API 控制器的输入验证上,enum 也更省事——模型绑定系统会自动拒绝未定义的枚举值,这点比原始整数要安全得多。
常见反模式
用字符串常量模拟 enum。如果你的代码里出现了 if (status == "Shipped"),这里应该用一个真正的 enum,而不是字符串常量。
把运行时用户定义的值做成 enum。CMS 里用户创建的分类名称不应该是 enum,它们是数据库里的查找表,应该用字符串列加关系表来处理。
相同常量定义在多处。如果 MaxRetries = 3 出现在三个文件里,应该集中到单个常量定义处。问题是重复本身,不是常量本身。
把携带行为的值放在 enum 里。一个 Color enum 需要 ToHex() 方法,说明它在向枚举类模式演进。用扩展方法挂上去能解决问题,但如果行为越来越多,枚举类更清晰。
决策流程
具体场景下,可以按这个顺序来判断:
- 是单个独立值(阈值、限制、标识符)?→
const或static readonly - 是互斥的有边界命名选项集合?→
enum - 需要同时组合多个值?→
[Flags] enum - 每个值需要关联数据或行为?→ 枚举类
- 值由运行时用户定义?→ 字符串 + 数据库查找表