你第一次学 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# 的高级玩法。