const在编译时替换为字面量,il中无字段访问指令;readonly在运行时确定值,支持任意类型且字段真实存在;二者语义不同,不可互换,跨程序集修改const需重新编译依赖方。

const 值在编译时就被替换成字面量
当你写 public const int MaxRetries = 3;,所有引用它的代码(比如 if (count > MaxRetries))在编译后都会变成 if (count > 3)。IL 中根本不存在对 MaxRetries 字段的读取指令,只有 ldc.i4.3 这类直接加载常量的操作。
这意味着:
- 如果这个
const定义在 NuGet 包 A 中,你项目 B 引用了它,之后你更新了包 A 把MaxRetries改成5,但没重新编译项目 B —— 那么 B 里依然用的是旧值3; -
const只能是编译期可求值的表达式,比如const string ApiRoot = "https://" + Domain;是错的,因为Domain不是 const; - 不能用于非基元类型或非字符串类型,
const List<int> Items = null;</int>合法(null 是允许的),但const List<int> Items = new List<int>();</int></int>编译不过。
readonly 字段在运行时才确定值,且支持任意类型
readonly 字段不是“编译期替换”,而是真实存在的字段,访问它会生成 ldfld 指令,走正常的字段读取流程。值可以来自构造函数、静态构造函数,甚至环境变量或配置文件。
常见使用场景包括:
- 依赖注入后的服务实例:
private readonly ILogger _logger;(在构造函数中赋值); - 运行时计算的值:
public static readonly DateTime BuildTime = DateTime.Now;; - 配置驱动的常量:
public readonly string ConnectionString = Configuration.GetConnectionString("Default");; - 支持结构体、自定义类、数组等:
public readonly ImmutableArray<string> ReservedNames = ImmutableArray.Create("id", "name");</string>。
readonly 引用类型字段 ≠ 对象内容不可变
这是最容易误解的一点:readonly 修饰的是“字段本身”,不是字段指向的对象。比如:
public readonly List<int> Numbers = new List<int>(); ... Numbers.Add(42); // ✅ 合法:修改对象内容 Numbers = new List<int>(); // ❌ 编译错误:试图替换字段引用 </int></int></int>
所以如果你需要真正不可变的对象,得配合 ReadOnlyCollection<t></t>、ImmutableArray<t></t> 或 readonly struct 使用,而不是只靠 readonly 关键字。
另外注意 CA2104 警告:对外暴露 public readonly List<t></t> 是不安全的,外部仍可修改集合内容,应改为 public readonly IReadOnlyList<t></t> 或封装 getter。
const 和 readonly 不能混用,也不能互相替代
它们底层语义完全不同,强行替换会导致行为断裂:
- 把
const string Version = "1.0";改成readonly string Version = "1.0";:调用方若用了字符串拼接(如$"v{Version}"),原先编译期就能确定的常量表达式会变成运行时拼接,可能影响内联或 JIT 优化; - 把
readonly int TimeoutMs = 5000;改成const int TimeoutMs = 5000;:如果这个值本应从 appsettings.json 读取,改 const 就彻底失去灵活性; - 试图在方法内声明
readonly int x = 10;:编译报错 ——readonly只能修饰字段,const才支持局部作用域。
最危险的是跨程序集场景下悄悄替换 const 为 readonly:表面看一切正常,但依赖方未重编译时,旧代码仍按字面量逻辑运行,新代码却走字段访问,可能导致条件判断失效或默认值错乱。











