游乐游手机版
首页/编程语言/文章详情

C#中const与readonly的区别详解及使用场景

时间:2026-05-10 16:56
const与readonly在C 中均用于定义不可修改的值,但存在本质区别。const是编译期常量,声明时必须赋值,值会内联到代码中,可能导致版本兼容性问题;readonly是运行时常量,可在声明或构造函数中赋值,修改后只需重新编译类库即可生效,版本更安全。此外,const可修饰字段和局部变量,默认静态;readonly仅修饰字段,默认实例成员。

在C#开发中,constreadonly这两个关键字经常被混为一谈,都笼统地称为“常量”。但如果你真把它们当成一回事,那可就踩到坑里了。它们俩在初始化规则、编译行为、版本兼容性等核心层面,存在着本质的区别。用错了,轻则引入隐蔽的逻辑错误,重则可能在类库升级时引发难以排查的版本陷阱。

浅谈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对引用类型非常挑剔,基本上只接受stringnull。原因无他,就是因为编译器必须在编译时就能确定其值,而除了字符串字面量,其他引用对象在编译期无法确定。

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 是 “构造期冻结的字段”。

结语

说到底,constreadonly的差异,远不止“一个能改一个不能改”那么简单。其本质区别在于三个关键问题:

  • 值在何时决定?(编译期 vs 运行期)
  • 是否被内联到IL?(这直接导致了版本兼容性问题)
  • 它保证了什么不可变?(值本身 vs 引用地址)

对于大多数C#开发者,尤其是涉及类库和模块化开发时,记住下面这个经验法则会帮你避开很多坑:

绝大多数需要对外暴露的“常量”,都应该优先考虑使用 readonly 而不是 const

能透彻理解并应用这一点,你对C#语言特性的把握就已经超越了只停留在语法层面的阶段。

来源:https://www.jb51.net/program/363574suw.htm
上一篇Go语言JSON、ProtoBuf与MsgPack序列化性能对比分析 下一篇Spring项目单元测试指南Junit与Maven集成实战
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
Java序列化中ObjectStreamField自定义字段控制详解
编程语言 · 2026-05-11

Java序列化中ObjectStreamField自定义字段控制详解

ObjectStreamField是描述序列化字段的元信息载体。通过声明serialPersistentFields数组并确保字段名、类型、顺序与类定义严格一致,可控制序列化字段。字段不匹配会导致静默反序列化失败。配合writeObject readObject方法可实现动态控制。应避免使用isUnshared、getOffset等底层方法。

实时操作系统RTOS线程调度与Java强实时变量处理对比分析
编程语言 · 2026-05-11

实时操作系统RTOS线程调度与Java强实时变量处理对比分析

实时操作系统(RTOS)通过优先级调度和中断机制确保微秒级确定性,而Java因垃圾回收、同步延迟和内存分配不确定性,难以满足强实时场景的严格时间要求,因此这类系统通常将核心逻辑交由RTOS处理。

Java并行流性能优化CollectorsgroupingByConcurrent方法详解
编程语言 · 2026-05-11

Java并行流性能优化CollectorsgroupingByConcurrent方法详解

Collectors groupingByConcurrent专为无需保持插入顺序、高并发写入的场景设计,能显著提升并行流分组性能。其底层通过所有线程直接写入同一个ConcurrentHashMap,避免了普通groupingBy的合并开销。适用于日志聚合、实时统计等高吞吐任务,但不适用于要求分组顺序的场景。使用时必须搭配并行流,且不支持自定义有序Map。在

循环队列数组实现详解头尾指针操作与取模运算实战指南
编程语言 · 2026-05-11

循环队列数组实现详解头尾指针操作与取模运算实战指南

循环队列通过数组实现,核心在于头尾指针的职责与取模运算。front指向队首,rear指向下一个空位,移动时需取模以确保回环。判空条件为front等于rear,判满则需牺牲一个存储单元。入队和出队操作后需立即取模,避免越界。动态内存管理时需注意分配与释放顺序,防止内存泄漏。

ThinkPHP入口文件配置参数修改与环境变量动态加载指南
编程语言 · 2026-05-11

ThinkPHP入口文件配置参数修改与环境变量动态加载指南

在ThinkPHP框架中动态调整数据库连接等配置参数,是许多开发者实现多环境部署的核心需求。然而,你是否曾遇到这样的困境:在入口文件中修改了配置值,刷新页面后却发现更改并未生效?这通常源于对框架配置加载机制的理解偏差。 本文将深入解析ThinkPHP配置生效的唯一正确路径,帮助你彻底规避“本地测试通