readonly是“赋值锁”而非“值锁”,仅禁止构造后重新赋值字段,不阻止修改引用类型对象的内部状态;对值类型字段则实现真正不可变。

readonly 字段不是“值锁”,而是“赋值锁”——它只禁止在构造函数之后对字段进行重新赋值,不阻止你修改字段所指向对象的内部状态。
readonly 字段能防止哪些修改
它只拦住直接赋值动作,比如 _items = new List<string>()</string> 或 _config = new Config()。但以下操作完全合法且常见:
- 如果字段是
readonly List<string> _items</string>,你可以调用_items.Add("x")、_items.Clear() - 如果字段是
readonly StringBuilder _sb,你可以调用_sb.Append("hello") - 如果字段是
readonly Dictionary<int string> _cache</int>,你可以调用_cache[1] = "v"
原因:readonly 修饰的是“引用本身”,不是“引用指向的对象”。只要对象是可变的(mutable),它的内容照改不误。
readonly struct 和普通 readonly 字段的区别
这是最容易混淆的点:两者语义完全不同。
-
readonly int _x:字段不可再赋新值,但若它是引用类型,其内容仍可变 -
readonly struct Point { public readonly int X; public readonly int Y; }:整个结构体实例不可变,所有字段(包括 private)都不能在构造后修改,连ref readonly返回后也不能写
典型报错:CS8342 "Cannot assign to field 'X' because it is a readonly field of a readonly variable" —— 这不是字段声明错了,而是你在 in Point p 或 ref readonly Point p 的上下文中试图改 p.X。
为什么 public readonly List 是危险设计
它看起来“只读”,实则完全暴露可变性。外部拿到后能任意增删,而你作为作者却无法感知或拦截。
- 错误写法:
public readonly List<string> Items = new();</string>→ 外部可直接Items.Add(...) - 正确封装:
private readonly List<string> _items = new(); public IReadOnlyList<string> Items => _items.AsReadOnly();</string></string> - 更优选择:
public IReadOnlyList<string> Items => _items.AsReadOnly();</string>(属性 getter 复用同一实例,避免重复包装)
注意:AsReadOnly() 不做拷贝,只是包装;如果 _items 在别处被改,Items 立刻反映变化。真要隔离,得用 new ReadOnlyCollection<string>(new List<string>(_items))</string></string>,但代价是内存和性能。
static readonly 和 const 的关键分界点
二者都常用于常量,但行为差异直接影响部署和调试:
-
const string ApiUrl = "https://api.example.com";→ 编译期内联,所有引用处直接替换成字符串字面量;改了库,不重编译调用方就看不到更新 -
static readonly string ApiUrl = Environment.GetEnvironmentVariable("API_URL") ?? "https://api.example.com";→ 运行时求值,支持配置驱动、环境切换、甚至DateTime.Now这类动态值
所以:需要编译期确定、永不变更的字面量(如数学常数 π),用 const;需要运行期初始化、可能随环境变化的“逻辑常量”,必须用 static readonly。
最常被忽略的一点:readonly 对 struct 和 class 的保护力度不对称——struct 能靠 readonly struct + readonly member 实现真正不可变,class 却只能靠约定和封装来模拟;别指望一个 public readonly MyClass _svc 就能防住状态污染。










