首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >别再以为 readonly 就是“不能改”了!C# 中的坑,90% 的人都踩过

别再以为 readonly 就是“不能改”了!C# 中的坑,90% 的人都踩过

作者头像
郑子铭
发布2025-09-02 18:12:19
发布2025-09-02 18:12:19
4020
举报

你第一次学 C# 的时候,看到 readonly 这个关键字,可能觉得它很简单:

“哦,就是这个字段只能赋值一次,之后不能再改。”

没错,对于 intstring 这种基础类型,确实是这样。 但一旦你开始用它来修饰一个类对象或者集合,你会发现: 咦?对象里的属性怎么还能改?!

别急,这不是编译器出 bug 了,而是你没真正搞懂 readonly 到底“只读”的是啥。

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


1️⃣ readonly 到底干了啥?

在 C# 里,readonly 的作用很明确:

  • 只能用在字段(field)上(不能用在属性)
  • 字段只能在声明时或构造函数中赋值
  • 一旦赋完值,就不能再指向别的对象

举个例子:

代码语言:javascript
复制
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 锁的是“引用”,不是“对象本身”


2️⃣ 值类型 vs 引用类型:readonly 行为大不同

✅ 值类型(int、bool、struct)

对于值类型来说,readonly 锁的是整个值:

代码语言:javascript
复制
private readonly int number = 5;

// ❌ 编译错误:不能改
number = 10;

因为值类型是“拷贝值”,所以 readonly 一锁,整个值就动不了。

✅ 引用类型(class、List)

而对于引用类型,情况就不一样了:

代码语言:javascript
复制
private readonly List<string> items = new List<string>();

// ✅ 允许:往集合里加东西
items.Add("Test");

// ❌ 不允许:不能换一个新集合
items = new List<string>();

看到了吗? 你可以往 itemsAddRemoveClear,都没问题。 但你不能写 items = new List<string>(),因为这等于“换了个新地址”。

👉 总结一下:

类型

readonly 锁住的是

值类型(int, struct)

整个值

引用类型(class, List)

引用地址(不能换对象)


3️⃣ 那我怎么才能让对象“真正不可变”?

如果你希望一个对象从创建之后,里面的数据谁也不能改,那就不能只靠 readonly

你需要组合拳:

  • 属性用 init,不用 set
  • 字段用 readonly
  • 集合不要暴露 List<T>,改用 IReadOnlyList<T>IReadOnlyCollection<T>
  • 优先使用 recordreadonly struct

✅ 正确示例:

代码语言:javascript
复制
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 创建完成,谁都改不了它的名字和角色,这才是真正的“不可变对象”。


4️⃣ readonly 和属性有啥关系?

你可能会想:能不能给属性加 readonly? 答案是:不能

代码语言:javascript
复制
public readonly string Name { get; set; } // ❌ 编译错误!

但你可以用 init 来模拟类似效果:

代码语言:javascript
复制
public string Role { get; init; } = "admin";

这样,Role 只能在对象初始化时设置,之后谁也不能改,效果和“只读属性”差不多。


🧪 实战测试:readonly 到底影响性能吗?

我们写一段代码,测试不同场景下 readonly 的表现。

代码语言:javascript
复制
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()
    {
        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,本地测试)

代码语言:javascript
复制
==== 值类型 & 引用类型 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不是“对象不可变”,而是“引用不能换”; 对于值类型,它锁的是值;对于引用类型,它只锁地址;要实现真正的不可变,得靠 initIReadOnlyXXXrecord 这些组合拳;在高性能场景,readonly structSpan 才是王道。

下次你写代码时,别再以为 private readonly List<User> users = new(); 就安全了—— 别人照样能往里面 Add 一百个用户!

真正安全的写法,是让“不能改”这件事,在编译期就拦住。这才是 C# 的高级玩法。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-09-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 DotNet NB 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1️⃣ readonly 到底干了啥?
    • ❗ 常见误解
  • 2️⃣ 值类型 vs 引用类型:readonly 行为大不同
    • ✅ 值类型(int、bool、struct)
    • ✅ 引用类型(class、List)
  • 3️⃣ 那我怎么才能让对象“真正不可变”?
    • ✅ 正确示例:
  • 4️⃣ readonly 和属性有啥关系?
  • 🧪 实战测试:readonly 到底影响性能吗?
    • 📊 运行结果(.NET 8,本地测试)
  • 📋 对比总结
  • ✅ 结语:别被 readonly 的名字骗了
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档