init-only setter 是 C# 9 引入的编译器级约束,允许属性仅在初始化上下文(构造函数、对象初始化器、with 表达式)中赋值一次;而 readonly 字段仅限声明或构造函数赋值,灵活性更低但运行时不可变。

init-only setter 是什么,和 readonly 有什么区别
init 访问器是 C# 9 引入的语法糖,它允许属性在对象初始化期间(构造函数、对象初始化器、with 表达式)被赋值一次,之后不可修改。它不是运行时强制的只读,而是编译器层面的约束:只要赋值发生在「初始化上下文」中就合法,否则报错 CS8852。
它和 readonly 字段不同:readonly 字段只能在声明或构造函数里赋值,而 init 属性支持对象初始化器写法,更灵活;但它又比 set 更安全,避免了后续意外修改。
常见错误现象:
- 用对象初始化器赋值后,在构造函数外再赋值 → 编译失败,提示
CS8852 - 在构造函数里调用
this.Property = value→ 合法(属于初始化上下文) - 在
Init方法里赋值 → 不合法,哪怕这个方法紧接在构造之后调用
怎么写一个带 init-only setter 的属性
语法就是把 set 换成 init:
public class Person
{
public string Name { get; init; }
public int Age { get; init; } = 0;
}关键点:
-
init可以和get组合,也可以单独存在(但没意义) - 可以有默认值(如
= 0),它会在初始化器未提供值时生效 - 如果类有自定义构造函数,仍可使用对象初始化器 —— 编译器会把初始化器代码合并进构造调用
- 不支持在
init访问器里写逻辑(比如验证),因为它是隐式实现的;如需逻辑,得用私有 backing field + 手动init实现
init-only 属性配合 with 表达式做不可变更新
with 是 C# 10 引入的特性,专为 init 属性设计,用于创建副本并修改部分字段:
var p1 = new Person { Name = "Alice", Age = 30 };
var p2 = p1 with { Age = 31 }; // ✅ 合法:生成新实例,Age 赋新值,Name 复制旧值注意限制:
-
with只能用于所有属性都有init或get的类型(即“记录式”行为) - 如果某个属性只有
get没有init,with不能改它,但可以改别的 -
with不触发任何 setter/init 逻辑 —— 它是编译器直接复制字段/属性值,跳过访问器
init-only setter 的实际适用场景和坑
适合建模「创建后不变」的数据载体,比如 DTO、配置快照、事件载荷、API 响应模型。
容易踩的坑:
- 误以为
init能防止反射修改 —— 实际上PropertyInfo.SetValue()依然能绕过,它只是编译期检查 - 在继承链中,基类
init属性无法被派生类的构造函数「再次初始化」,除非显式调用base(...)并在初始化器中赋值 - JSON 反序列化(如 System.Text.Json)默认不支持
init属性赋值,需开启PropertyNameCaseInsensitive = true和IncludeFields = true等配置,或改用 Newtonsoft.Json(需[JsonConstructor]配合) - EF Core 6+ 支持
init属性映射,但迁移生成可能出错,建议显式标注[DatabaseGenerated(DatabaseGeneratedOption.None)]
init-only setter 看似简单,但它的边界全由编译器定义,而不是运行时保护。一旦离开初始化上下文(哪怕只是多包一层方法调用),赋值就会失败 —— 这个「上下文」的判定规则,比看起来更微妙。










