news 2026/6/21 6:38:39

readonly 和 const 的区别,不只是能不能修改

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
readonly 和 const 的区别,不只是能不能修改

一、引言:常量设计的常见误解

表面差异:「能否修改」只是冰山一角

很多开发者对 const 和 readonly 的理解停留在:

constintA=1;readonlyintB=1;

二者似乎都代表“不可修改”。

于是很多面试中也会出现这样一个经典问题:

const 和 readonly 有什么区别?

得到的答案通常是:

const 是常量,readonly 是只读字段。

这样的回答虽然不能算错,但远远没有触及问题本质。

实际上:

  • const 解决的是编译期确定性(Compile-Time Determinism)
  • readonly 解决的是对象生命周期内的引用稳定性(Reference Stability)

二者在 CLR 中拥有完全不同的实现方式。甚至从 IL 角度看,它们都不是同一种东西。

真实场景

如果你做过大型项目,可能遇到过下面这些问题:

  • 为什么修改了public const int Timeout = 30;重新发布 DLL 后,调用方仍然输出旧值?
  • 为什么readonly List<int> ids = new();仍然能够执行ids.Add(1);
  • 为什么微软官方设计指南中建议:尽量避免公开暴露 public const

理解这些问题之后,你会发现 const 和 readonly 根本不是同一层面的设计。


二、核心差异深度解析

初始化的本质

虽然二者都表现为“不可修改”,但初始化时机完全不同。

const:编译时初始化

const 的值必须在编译阶段就能确定。

publicconstintMaxRetry=3;

这是合法的。但下面的代码会直接编译失败:

publicconstintMaxRetry;publicDemo(){MaxRetry=3;}

原因很简单:const 的值必须在编译阶段确定。对于编译器而言,const int MaxRetry = 3;本质上已经不是一个普通字段,而是一个能够被直接替换的字面量。

readonly:运行时初始化

readonly 属于运行时概念。

既可以使用字段初始化器:

publicreadonlystringApiKey=LoadConfig();

也可以在构造函数中赋值:

publicreadonlystringApiKey;publicDemo(){ApiKey=LoadConfig();}

但一旦对象初始化结束,在普通方法中再次赋值:

publicvoidChange(){ApiKey="NewValue";}

编译器会立即报错:readonly 字段只能在声明处或构造函数中赋值。因为 readonly 限制的是赋值时机,而不是编译期常量。


类型行为的分歧

const 仅支持编译期常量类型

并不是所有类型都能声明为 const。

publicconstintRetryCount=3;publicconstdecimalTaxRate=0.13m;publicconststringAppName="Demo";

这些合法,因为它们属于编译期常量。

常见支持类型包括:bool、char、byte、sbyte、short、ushort、int、uint、long、ulong、float、double、decimal、string、enum。

例如public const DayOfWeek StartDay = DayOfWeek.Monday;完全合法。

但下面无法通过编译:

publicconstDateTimeMinDate=newDateTime(2024,1,1);

因为 DateTime 不是编译期常量类型,即使这个对象永远不会改变,也无法成为 const。new DateTime(...)必须在运行时执行。

需要特别注意的类型限制细节:

  • const string可以赋值为null,例如const string? s = null;合法。
  • 但其他引用类型即使为null也不能声明为const,例如const Uri uri = null;会编译错误,因为Uri不是编译期常量类型。
  • 本质原因:const 要求值能在编译时完全确定,且类型必须是基元类型、string或枚举,任何自定义引用类型都不满足要求。
readonly 支持任意类型

readonly 没有这种限制。

publicreadonlyDateTimeStartTime;publicreadonlyHttpClientClient;publicreadonlyList<int>ValidIds;

全部合法,因为 readonly 本质仍然是字段,只是增加了赋值限制而已。

实例级与类型级差异

除了初始化时机不同,const、static readonly 与 readonly 的作用域也完全不同。

类型作用域访问方式
const类型级别ClassName.Field
static readonly类型级别ClassName.Field
readonly实例级别instance.Field

例如:

publicclassConfig{publicconstintConstValue=1;publicstaticreadonlyintStaticValue=2;publicreadonlyintInstanceValue;publicConfig(intvalue){InstanceValue=value;}}

访问方式:

Config.ConstValue;Config.StaticValue;varconfig=newConfig(100);config.InstanceValue;

从内存模型角度看:const 和 static readonly 属于类型,readonly 属于对象实例。因此所有对象共享同一个 const 或 static readonly,但每个对象都可以拥有不同的 readonly 值。这也是为什么new Config(1024)new Config(2048)能够产生不同实例状态。

作用域与存储机制

const 隐含 static

很多开发者忽略了这一点。

publicconstintMaxRetry=3;

虽然没有写static,但Config.MaxRetry是合法的写法,而new Config().MaxRetry只是编译器允许的语法糖。从设计角度看,const 天生属于类型而不是对象,因为它根本不需要实例存在。

需要明确:const 实际等价于同时拥有staticconst修饰,但不能显式写出static关键字。例如public const int Max = 100;的效果等同于假设中的public static const int Max = 100;(该语法不合法,但语义如此)。const 字段始终是类型级别的成员,永远不会属于某个实例。

readonly 默认是实例字段
publicreadonlyintBufferSize;

访问时必须通过实例:

varconfig=newConfig();Console.WriteLine(config.BufferSize);

不同对象可以拥有不同值。当然,也可以声明为public static readonly int BufferSize = 1024;,此时字段属于类型级别。


编译时的魔法替换

这是 const 最重要、也最容易被忽视的特性。

假设:

publicconstintMaxRetry=3;Console.WriteLine(Config.MaxRetry);

很多人以为运行时会读取字段,实际上编译器会直接替换为Console.WriteLine(3);,调用方可能根本不会读取这个字段。

从 IL 可以看得更明显。对于Console.WriteLine(Config.MaxRetry);,生成的 IL 类似:

ldc.i4.3 call void [System.Console]Console::WriteLine(int32)

ldc的意思是 Load Constant,直接把常量压栈,没有字段读取动作。

而如果改成public static readonly int MaxRetry = 3;,IL 会变成:

ldsfld int32 Config::MaxRetry call void [System.Console]Console::WriteLine(int32)

ldsfld即 Load Static Field,运行时真正访问字段。

为什么 CLR 要这样设计?原因在于性能与优化。

例如:

constintA=10;constintB=20;constintC=A+B;

编译器直接计算得到30,最终 IL 只是ldc.i4.s 30,无需运行时参与。

这带来了常量折叠、死代码消除、更简单的执行路径和更高的 JIT 优化空间。

const 的核心价值是编译期优化,而不是“不可修改”。


三、陷阱与最佳实践

版本兼容性陷阱

这是最经典的生产事故来源之一。

假设两个程序集:

程序集 A

publicconstintTimeout=30;

程序集 B

Console.WriteLine(Config.Timeout);

编译 B 时,Console.WriteLine(30);已经被写入 IL。此时程序集 B 不再依赖字段本身。

后来将程序集 A 修改为public const int Timeout = 60;并重新发布,但没有重新编译程序集 B,结果 B 仍然输出 30。这就是著名的 const 跨程序集版本陷阱。

微软为什么不推荐 public const

对于公共 API,public const int Timeout = 30;一旦发布,调用方就会把值嵌入自己的程序集。后续修改数值不会自动生效,必须重新编译所有调用方,这会导致非常隐蔽的版本问题。

解决方案:公共 API 推荐使用public static readonly int Timeout = 30;,因为 readonly 始终在运行时读取真实值,调用方升级 DLL 后即可获得新值,无需重新编译。


动态初始化场景

很多配置天然无法使用 const,例如:配置文件、环境变量、数据库配置、依赖注入、运行时计算等。

publicreadonlystringConnectionString;publicConfig(IConfigurationconfiguration){ConnectionString=configuration.GetConnectionString("Default");}

这是 readonly 的典型应用场景,而 const 根本无法完成。


集合的特殊性

这是很多人的第二个认知误区。

privatereadonlyList<int>_ids=new();

很多人认为集合不能修改,实际上完全错误。不能做的是_ids = new List<int>();,因为引用发生变化。但_ids.Add(1)_ids.Add(2)_ids.Clear()全都合法。readonly 限制的是引用变化,而不是对象状态变化。

readonly 并非绝对不可修改

readonly 的约束主要由编译器保证。正常代码无法再次赋值:

publicreadonlystringKey="SECRET";// 编译错误Key="NEW";

但通过反射仍然能够绕过限制,这可能带来安全隐患:

publicclassConfig{publicreadonlystringSecret="DATA";}varinstance=newConfig();varfield=typeof(Config).GetField("Secret");field!.SetValue(instance,"HACKED");Console.WriteLine(instance.Secret);// 输出 HACKED

这是因为 readonly 并不是 CLR 层面的绝对不可变,而是一种字段写入约束,反射拥有绕过普通访问限制的能力。

不过,生产代码不应该依赖这种行为。这种技术主要出现在测试框架、Mock 框架、ORM、序列化框架等特殊场景。

readonly 保证的是正常程序路径下的安全性,而不是防御恶意代码。

这与 Java 中的final List<Integer>本质相同。


IReadOnlyList<T> 与 ImmutableList<T>

如果希望真正表达不可变语义,仅仅readonly List<T>远远不够。

IReadOnlyList<T>表示调用方无法修改集合。例如public IReadOnlyList<int> Items => _items;调用方无法执行Add()Remove()Clear(),但底层_items.Add(...)依然可以发生。

ImmutableList<T>(来自System.Collections.Immutable)才能真正做到数据不可变:

ImmutableList<int>ids=ImmutableList<int>.Empty;ids=ids.Add(1);

这里不会修改原集合,而是返回新实例。

三者含义完全不同:

方案含义
readonly List<T>引用不可变
IReadOnlyList<T>接口不可写
ImmutableList<T>数据真正不可变

使用枚举代替相关常量组

当多个常量表达同一组状态时:

不推荐

publicconstintState_Active=1;publicconstintState_Inactive=2;publicconstintState_Deleted=3;

更推荐

publicenumUserState{Active=1,Inactive=2,Deleted=3}

原因在于:类型安全更强、可读性更好、更容易扩展、避免常量污染命名空间。

因此,单个固定值适合 const,一组具有关联关系的常量更适合 enum。


四、实战代码:综合应用示例

下面是一段实际项目中比较合理的设计:

publicclassAppConstants{// 编译期常量publicconststringAppName="C# Core";// 运行时字段publicstaticreadonlyDateTimeStartTime=DateTime.Parse("2024-01-01");// 依赖注入publicreadonlyDbConnectionConnection;// 安全只读暴露publicstaticreadonlyIReadOnlyList<string>Countries=newList<string>{"US","CN"}.AsReadOnly();publicAppConstants(DbConnectionconnection){Connection=connection;}}

这段代码同时体现了:const 用于真正编译期常量,static readonly 用于公共共享数据,readonly 用于依赖注入对象,IReadOnlyList 用于安全暴露集合。


五、总结

选型指南

场景首选原因
永远不会变化的编译期常量const支持编译期优化
运行时动态初始化readonly灵活且安全
公共 API 常量static readonly避免版本兼容问题
复杂类型readonlyconst 无法支持
真正不可变集合ImmutableList<T>数据不可变

核心认知

const 关注的是Value(值),readonly 关注的是Identity(引用),enum 关注的是Domain(领域状态)。三者解决的是完全不同的问题:

  • const:编译期常量
  • readonly:生命周期内的引用稳定性
  • enum:业务状态建模

理解这一点,才能在实际项目中正确选择它们。


示意图设计建议

图一:const 与 readonly 的 IL 对比
const │ ▼ literal │ ▼ ldc.i4.3 │ ▼ 直接使用字面量 ------------------ readonly │ ▼ initonly │ ▼ ldfld / ldsfld │ ▼ 运行时读取字段
图二:跨程序集版本陷阱
Library v1 │ ▼ public const Timeout = 30 │ ▼ App 编译 │ ▼ 写入字面量 30 │ ▼ Library v2 │ ▼ public const Timeout = 60 │ ▼ App 未重新编译 │ ▼ 仍然输出 30

公共 API 优先使用 static readonly 而不是 public const——对于库设计者来说,版本兼容性往往比那一点点编译期优化更重要。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/8 0:50:39

CAXA 软件常用设置

位置12恢复缺省设置【作用】点击后&#xff0c;会重置软件初始的设置。导出为文件将软件设置&#xff0c;保存为.xml 文件。从文件导入从 .xml 文件恢复设置。【作用】其它电脑可复用软件参数设置。O1 路径操作【打开】就是打开这个路径。【修改】修改模板所在文件夹路径。&…

作者头像 李华
网站建设 2026/6/6 0:51:13

如何在3天内掌握Python信用评分卡开发:从零到精通的完整教程

如何在3天内掌握Python信用评分卡开发&#xff1a;从零到精通的完整教程 【免费下载链接】scorecardpy Scorecard Development in python, 评分卡 项目地址: https://gitcode.com/gh_mirrors/sc/scorecardpy 在金融风控领域&#xff0c;Python信用评分卡已成为风险评估的…

作者头像 李华
网站建设 2026/6/8 2:17:47

如何免费解锁百度网盘SVIP功能:macOS用户终极提速指南

如何免费解锁百度网盘SVIP功能&#xff1a;macOS用户终极提速指南 【免费下载链接】BaiduNetdiskPlugin-macOS For macOS.百度网盘 破解SVIP、下载速度限制~ 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduNetdiskPlugin-macOS 您是否经常为百度网盘在macOS上的下载…

作者头像 李华