Skip to content
Go back

C# 中 Enum 与常量怎么选:一份实用决策指南

C# Enum 与常量决策指南封面

在 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

典型例子:

// 星期 —— 封闭、有限、互斥
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 也是更自然的选择。

适合用常量的场景

满足以下任意条件时,选 conststatic readonly

// 配置常量 —— 独立,没有类型关联
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 readonlyenum
类型安全
穷举性 switch是(CS8509)
遍历所有值Enum.GetValues<T>()
扩展性需要重新编译
字符串序列化原生支持需要 JsonStringEnumConverter
每个值携带行为否(此时用枚举类)
编译期常量仅成员本身(变量不是)

经验法则:只要你会对这个值写 switch,就用 enum;如果它是单个阈值或标识符,就用常量。

第三个选项:枚举类

有时候 constenum 都不够用。枚举类(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() 方法,说明它在向枚举类模式演进。用扩展方法挂上去能解决问题,但如果行为越来越多,枚举类更清晰。

决策流程

具体场景下,可以按这个顺序来判断:

  1. 是单个独立值(阈值、限制、标识符)?→ conststatic readonly
  2. 是互斥的有边界命名选项集合?→ enum
  3. 需要同时组合多个值?→ [Flags] enum
  4. 每个值需要关联数据或行为?→ 枚举类
  5. 值由运行时用户定义?→ 字符串 + 数据库查找表

参考


Tags


Next

在 ASP.NET Core 中为 SignalR Hub 添加 JWT 身份认证