C#中const与readonly的区别详解及使用场景
在C#开发中,const和readonly这两个关键字经常被混为一谈,都笼统地称为“常量”。但如果你真把它们当成一回事,那可就踩到坑里了。它们俩在初始化规则、编译行为、版本兼容性等核心层面,存在着本质的区别。用错了,轻则引入隐蔽的逻辑错误,重则可能在类库升级时引发难以排查的版本陷阱。
免费影视、动漫、音乐、游戏、小说资源长期稳定更新! 👉 点此立即查看 👈

一、初始化位置:编译时强约束 vs 运行时一次性赋值
这是最直观的区别,决定了你什么时候、在哪儿能给它们赋值。
1. const:必须声明即赋值(编译期确定)
对于const,规矩很死板:它必须在声明的那一刻就给出最终值,这个值在编译阶段就必须是板上钉钉的。之后,在任何地方——包括构造函数——你都别想再动它分毫。
public class ConstantDemo
{
public const int MaxRetryCount = 3;
public const string DefaultTitle = "C#常量解析";
// ❌ 编译错误:声明时未赋值
// public const double Pi;
// ❌ 编译错误:不能在构造函数中修改
// public ConstantDemo()
// {
// MaxRetryCount = 5;
// }
}
一句话总结:
const是一个“声明即终值”的编译期常量。
2. readonly:声明时或构造函数中赋值(运行期确定)
readonly就灵活多了。它给你两个选择:要么在声明时直接赋值,要么在构造函数(实例构造函数或静态构造函数)里完成赋值。但记住,不管选哪种,每个readonly字段一生只有这一次赋值机会。
public class ReadonlyDemo
{
// 声明时赋值
public readonly int MinAge = 18;
// 构造函数中赋值
public readonly int UserId;
public ReadonlyDemo(int userId)
{
UserId = userId;
}
// 静态 readonly:在静态构造函数中赋值
public static readonly string Version;
static ReadonlyDemo()
{
Version = "1.0.1";
}
}
一句话总结:
readonly是一个“构造期冻结”的运行时常量。
二、修饰对象范围:字段 + 局部变量 vs 仅字段
它们能管到的地方也不一样。
const的权限比较大,既能修饰类的字段,也能在方法内部作为局部常量使用。
public class ConstScopeDemo
{
public const int GlobalConst = 100;
public void LocalConstDemo()
{
const string LocalMsg = "局部常量";
Console.WriteLine(LocalMsg);
}
}
而readonly就“专一”得多,它只能修饰类的字段,想用在方法内部?编译器直接报错。
public class ReadonlyScopeDemo
{
public readonly int FieldReadonly = 50;
public void LocalReadonlyError()
{
// ❌ 编译错误:readonly 不能修饰局部变量
// readonly int x = 10;
}
}
三、编译期 vs 运行期:这是最本质的差异
前面的区别都是表象,下面要说的才是决定它们行为差异的根源,也是工程中最重要的考量点。
1. const:值被直接“内联”到 IL 中
看这段简单的代码:
public const int ConstValue = 10;
public void UseConst()
{
int a = ConstValue;
}
它的IL(中间语言)行为本质是这样的:
ldc.i4.s 10 // 直接压栈常量 10
看到了吗?编译器在生成IL时,直接把ConstValue这个符号替换成了字面量10。这意味着,所有引用这个const的地方,拿到的都是硬编码的10。
⚠️ 重大隐患(版本陷阱):
- 假设你发布了一个类库,里面有个
public const int MaxCount = 100;。- 后来你把值改成了
200,并重新编译发布了新版本的类库(DLL)。- 但是,引用你这个类库的应用程序如果没有重新编译,它里面所有用到
MaxCount的地方,依然被硬编码为100。- 结果就是:你更新了库,但调用方还在用旧值,版本不一致的问题就此产生。
2. readonly:始终通过字段访问(运行期绑定)
再看readonly:
public readonly int ReadonlyValue;
public ReadonlyDemo()
{
ReadonlyValue = 20;
}
public void UseReadonly()
{
int b = ReadonlyValue;
}
其IL行为是:
ldfld int32 ReadonlyValue
这里是在运行时去加载ReadonlyValue这个字段的值。所以,当你修改类库中readonly字段的值并重新编译后,只要调用方加载的是新版本的DLL,它获取到的就是新值。
✅ 关键优势:修改值后,只需重新编译类库即可,调用方无需重新编译,完美避免了
const的版本陷阱。
四、静态语义:隐式静态 vs 显式静态
在静态特性上,两者也走了不同的路。
const天生就是静态的,你甚至不能用static关键字再去修饰它,因为多此一举。调用时也必须通过类名。
public class ConstStaticDemo
{
public const int ConstStatic = 10;
// ❌ 编译错误:不能重复声明static
// public static const int Invalid = 20;
}
// 调用方式
int x = ConstStaticDemo.ConstStatic;
readonly则默认是实例级别的,每个对象可以有自己的值。如果你需要的是一个全类共享的、只读的静态字段,必须显式加上static关键字。
public class ReadonlyStaticDemo
{
public readonly int InstanceReadonly = 100; // 每个实例不同
public static readonly int StaticReadonly = 200; // 全局唯一
}
五、引用类型语义:值不可变 vs 引用不可变
当修饰引用类型时,两者的区别更加微妙。
const对引用类型非常挑剔,基本上只接受string和null。原因无他,就是因为编译器必须在编译时就能确定其值,而除了字符串字面量,其他引用对象在编译期无法确定。
public class ConstReferenceDemo
{
public const string ConstString = "Hello";
public const object ConstNull = null;
// ❌ 编译错误:非编译期常量
// public const List ConstList = new List();
}
readonly则友好得多,它可以修饰任意引用类型。但这里有一个至关重要的理解:readonly锁住的是引用(即内存地址)本身不可变,而不是锁住对象内部的内容。
public class ReadonlyReferenceDemo
{
public readonly List Numbers = new() { 1, 2, 3 };
public void Modify()
{
Numbers.Add(4); // ✅ 合法:修改对象内容
// ❌ 编译错误:不能重新赋值(改变引用)
// Numbers = new List();
}
}
⚠️ 重要提醒:
readonly≠ 不可变对象
- 它保证的是字段指向的引用地址不变。
- 它不保证所引用对象内部状态不变。如果你需要真正的不可变集合,应该使用
System.Collections.Immutable命名空间下的类型。
六、完整对比速查表
| 维度 | const | readonly |
|---|---|---|
| 初始化时机 | 编译期 | 运行期 |
| 赋值位置 | 仅声明处 | 声明 / 构造函数 |
| 修饰对象 | 字段 + 局部变量 | 仅字段 |
| 静态特性 | 默认 static | 默认实例级 |
| IL 行为 | 内联常量 | 字段访问 |
| 引用类型 | string / null | 任意引用类型 |
| 版本安全 | ❌ 易出问题 | ✅ 安全 |
七、工程化使用建议
理解了原理,该怎么用就清晰了。下面是一些实战指南。
优先使用 const 的场景
- 真正的数学或逻辑常量:例如圆周率
Math.PI、自然常数Math.E,这些值永恒不变。 - 内部使用的字面量:仅在当前程序集内部使用、且绝对不变的常量,比如一个内部算法的固定系数。
- 不会对外暴露的枚举替代值:一些简单且不会变化的选项值。
public const int MaxRetryAttempts = 3;
推荐使用 readonly 的场景(真实项目中更常见)
- 类库或API对外暴露的“常量”:这是最重要的原则。只要你的常量可能被其他程序集引用,就应该用
readonly来避免版本陷阱。 - 依赖运行时配置的值:比如从配置文件、数据库或环境变量中读取的配置项。
- 需要通过构造函数注入的参数:常用于依赖注入场景,确保对象在构造后某些状态不可变。
- 引用类型的只读字段:比如一个在类初始化时创建,之后不允许替换但内容可变的集合。
public class AppSettings
{
public static readonly string ApiEndpoint;
static AppSettings()
{
ApiEndpoint = ConfigurationManager.AppSettings["ApiEndpoint"];
}
}
八、一句话记忆法
面试或快速回顾时,可以这样记:
const 是 “编译期写死的字面量”。
readonly 是 “构造期冻结的字段”。
结语
说到底,const和readonly的差异,远不止“一个能改一个不能改”那么简单。其本质区别在于三个关键问题:
- 值在何时决定?(编译期 vs 运行期)
- 是否被内联到IL?(这直接导致了版本兼容性问题)
- 它保证了什么不可变?(值本身 vs 引用地址)
对于大多数C#开发者,尤其是涉及类库和模块化开发时,记住下面这个经验法则会帮你避开很多坑:
绝大多数需要对外暴露的“常量”,都应该优先考虑使用
readonly而不是const。
能透彻理解并应用这一点,你对C#语言特性的把握就已经超越了只停留在语法层面的阶段。
热门专题
热门推荐
冥山山脉地处世界最东端,终年浓雾笼罩,地势险峻、妖魔横行,属高危区域。挑战者需精心准备角色、装备与补给,谨慎规划路线,摸清地形并避开威胁。途中应收集资源、解锁地图、掌握妖魔弱点,善用道具、管理状态,也可借助门派支持。综合运用这些策略,方能提升生存几率,成功征服险境。
本文详细介绍了在OKX平台完成注册与登录的完整流程。从下载官方应用开始,逐步引导用户完成手机号或邮箱验证、设置安全密码等开户步骤。同时,也涵盖了后续的登录方法、基础安全设置建议,以及遇到常见问题时的解决思路,旨在为用户提供清晰、安全的入门指引。
本文介绍了欧乙交易平台现货页面的基本布局与核心功能,重点解析了行情图表、深度图、委托订单簿等关键区域的查看方法。详细说明了限价单、市价单等不同类型委托单的挂单策略与操作步骤,包括如何设置价格与数量。旨在帮助用户快速理解现货交易界面,掌握基础的挂单技巧,提升交易操作的效率与准确性。
本文梳理了OKX欧易平台用户可能遇到的几类常见问题,包括账号登录异常、身份验证失败以及交易限额查询。针对每种情况,分析了可能的原因,如网络环境、设备安全、信息填写错误等,并提供了清晰的自查与解决步骤。同时,详细说明了如何在平台内查看不同资产与交易对的当前限额,帮助用户更顺畅地进行数字资产管理和交易操作。
本文详细介绍了在欧意OKX平台进行安全设置的核心步骤,重点讲解谷歌验证器、资金密码与防钓鱼码的配置方法与重要性。通过分步指南与实用建议,帮助用户建立多层次防护体系,有效防范钓鱼攻击与未授权访问,保障数字资产安全。强调定期检查与安全意识是维护账户长期安全的关键。





