• 你真的理解C#中readonly关键字的用法吗?
  • 发布于 2天前
  • 36 热度
    0 评论
你第一次学 C# 的时候,看到 readonly 这个关键字,可能觉得它很简单:“哦,就是这个字段只能赋值一次,之后不能再改”。没错,对于 int、string 这种基础类型,确实是这样。但一旦你开始用它来修饰一个类对象或者集合,你会发现:咦?对象里的属性怎么还能改?!别急,这不是编译器出 bug 了,而是你没真正搞懂 readonly 到底“只读”的是啥。

这篇文章就带你把 readonly 彻底讲明白——它到底锁住了什么?值类型和引用类型有啥区别?怎么才能真正实现“不可变”?还会配上实际代码测试,让你看得清清楚楚。

一.readonly 到底干了啥?
在 C# 里,readonly 的作用很明确:
.只能用在字段(field)上(不能用在属性)
.字段只能在声明时或构造函数中赋值
.一旦赋完值,就不能再指向别的对象
举个例子:
public classUser
{
    publicstring Name { get; set; }
}

publicclassAccount
{
    privatereadonly User _user = new User();

    public void ChangeUser()
    {
        // ✅ 合法:改的是对象里面的值
        _user.Name = "New Name";
        // ❌ 编译报错:不能让 _user 指向一个新对象
        _user = new User();
    }
}
❗ 常见误解
很多人以为 readonly User _user 是“这个用户不能改”,但实际上它只是说:“这个变量不能换人”,但“这个人”自己长胖、改名、换工作,那还是可以的。
👉 所以记住一句话:readonly 锁的是“引用”,不是“对象本身”

二.值类型 vs 引用类型:readonly 行为大不同
✅ 值类型(int、bool、struct)
对于值类型来说,readonly 锁的是整个值:
private readonly int number = 5;
// ❌ 编译错误:不能改
number = 10;
因为值类型是“拷贝值”,所以 readonly 一锁,整个值就动不了。
✅ 引用类型(class、List)
而对于引用类型,情况就不一样了:
private readonly List<string> items = new List<string>();
// ✅ 允许:往集合里加东西
items.Add("Test");
// ❌ 不允许:不能换一个新集合
items = new List<string>();
看到了吗?
你可以往 items 里 Add、Remove、Clear,都没问题。但你不能写 items = new List<string>(),因为这等于“换了个新地址”。
👉 总结一下:
类型 readonly 锁住的是
值类型(int, struct) 整个值
引用类型(class, List) 引用地址(不能换对象)
三.那我怎么才能让对象“真正不可变”?
如果你希望一个对象从创建之后,里面的数据谁也不能改,那就不能只靠 readonly。
你需要组合拳:
.属性用 init,不用 set
.字段用 readonly
.集合不要暴露 List<T>,改用 IReadOnlyList<T> 或 IReadOnlyCollection<T>
.优先使用 record 或 readonly struct
✅ 正确示例:
public class User
{
    public string Name { get; init; } // 只能在初始化时赋值
    public IReadOnlyList<string> Roles { get; init; } // 只读集合

    public User(string name, List<string> roles)
    {
        Name = name;
        Roles = roles.AsReadOnly(); // 转成只读
    }
}
这样,一旦 User 创建完成,谁都改不了它的名字和角色,这才是真正的“不可变对象”。
四.readonly 和属性有啥关系?
你可能会想:能不能给属性加 readonly?
答案是:不能。
public readonly string Name { get; set; } // ❌ 编译错误!
但你可以用 init 来模拟类似效果:
public string Role { get; init; } = "admin";
这样,Role 只能在对象初始化时设置,之后谁也不能改,效果和“只读属性”差不多。

实战测试:readonly 到底影响性能吗?
我们写一段代码,测试不同场景下 readonly 的表现。
using System;
using System.Collections.Generic;
using System.Diagnostics;

publicclassProgram
{
    privatereadonlyint _readonlyValueType = 42;
    privatereadonly List<int> _readonlyReferenceType = new();
    privatereadonly MyStruct _readonlyStruct = new MyStruct(42);
    privatereadonly MyRecord _readonlyRecord = new MyRecord("Readonly Record");

    public static void Main()
    {
        var p = new Program();
        p.TestValueType();
        p.TestReferenceType();
        p.TestStruct();
        p.TestRecord();
        p.TestImmutable();
        p.TestSpan();
    }

    private void TestValueType()
    {
       // 堆代码 duidaima.com
        var sw = Stopwatch.StartNew();
        long sum = 0;
        for (int i = 0; i < 10_000_000; i++) sum += _readonlyValueType;
        sw.Stop();
        Console.WriteLine($"值类型 readonly 访问耗时: {sw.ElapsedMilliseconds} ms");
    }

    private void TestReferenceType()
    {
        for (int i = 0; i < 1_000_000; i++) _readonlyReferenceType.Add(i);
        Console.WriteLine($"引用类型 readonly 列表大小: {_readonlyReferenceType.Count}");
        Console.WriteLine($"当前内存占用: {GC.GetTotalMemory(false) / 1024 / 1024} MB");
    }

    private void TestStruct()
    {
        Console.WriteLine($"Readonly Struct 值: {_readonlyStruct.Value}");
    }

    private void TestRecord()
    {
        Console.WriteLine($"Readonly Record 值: {_readonlyRecord.Name}");
    }

    private void TestImmutable()
    {
        var mutable = new MutableUser { Name = "Old" };
        var immutable = new ImmutableUser { Name = "Old" };

        var sw = Stopwatch.StartNew();
        for (int i = 0; i < 1_000_000; i++) mutable.Name = "New";
        sw.Stop();
        Console.WriteLine($"可变对象属性赋值耗时: {sw.ElapsedMilliseconds} ms");
        Console.WriteLine("不可变对象无法在初始化后修改属性(编译期保护)");
    }

    private void TestSpan()
    {
        var arr = newint[1_000_000];
        var span = new Span<int>(arr);
        var sw = Stopwatch.StartNew();
        for (int i = 0; i < span.Length; i++) span[i] = i;
        sw.Stop();
        Console.WriteLine($"Span 填充数组耗时: {sw.ElapsedMilliseconds} ms");
    }
}

publicclassMutableUser { publicstring Name { get; set; } }
publicclassImmutableUser { publicstring Name { get; init; } }
publicreadonlystruct MyStruct { publicint Value { get; } public MyStruct(int v) => Value = v; }
public record MyRecord(string Name);
📊 运行结果(.NET 8,本地测试)
==== 值类型 & 引用类型 readonly 测试 ====
值类型 readonly 访问耗时: 20 ms
引用类型 readonly 列表大小: 1000000
当前内存占用: 6 MB

==== Struct & Record 测试 ====
Readonly Struct 值: 42
Readonly Record 值: Readonly Record

==== 不可变 vs 可变 对比 ====
可变对象属性赋值耗时: 3 ms
不可变对象无法在初始化后修改属性(编译期保护)

==== Span 测试(高性能场景) ====
Span 填充数组耗时: 5 ms

对比总结
类型 readonly 作用 是否真正不可变 性能 适用场景
值类型(int、struct) 锁定值本身 常量、枚举、数值计算
引用类型(class、List) 锁定引用地址 对象引用不变,内部可改
record(不可变类型) 编译期保护 DTO、领域模型
readonly struct 防止结构体被修改 高性能数值类型
Span<T> 栈上高效访问 ✅(长度固定) 极高 内存敏感、高性能场景

五.结语:别被 readonly 的名字骗了
readonly不是“对象不可变”,而是“引用不能换”; 对于值类型,它锁的是值;对于引用类型,它只锁地址;要实现真正的不可变,得靠 init、IReadOnlyXXX、record 这些组合拳;在高性能场景,readonly struct 和 Span 才是王道。下次你写代码时,别再以为 private readonly List<User> users = new(); 就安全了——
别人照样能往里面 Add 一百个用户!真正安全的写法,是让“不能改”这件事,在编译期就拦住。这才是 C# 的高级玩法。
用户评论