init关键字定义init-only setter,仅允许在对象初始化器、构造函数体及基类构造链中赋值,之后任何修改(含类内)均触发CS8852错误;它不等价readonly字段或private set属性,且required必须配合init使用。

init 关键字不是“只读属性初始化器”的别名,它定义的是 init-only setter —— 一种仅在对象构造阶段(含对象初始化器和构造函数体内)可赋值、之后彻底禁止修改的访问器。它不等价于 readonly 字段,也不等同于只有 get 的属性。
init 属性只能在对象初始化阶段赋值,否则编译报错 CS8852
这是最常遇到的错误现象:写完对象初始化后,再尝试修改 init 属性,比如:
var p = new Person { Name = "Alice", Age = 30 };
p.Age = 31; // ❌ 编译错误:CS8852 — Init-only property or indexer 'Person.Age' can only be assigned in an object initializer...原因很直接:init 访问器被编译器标记为仅限“初始化上下文”调用,这个上下文包括:
- 对象初始化器(
new Person { ... }内部) - 类自身的构造函数体(
this.Name = "Bob"合法) - 基类构造函数链中对派生类
init属性的赋值(需显式调用)
但不包括:任何方法调用、属性重设、反射赋值(除非绕过运行时检查)、序列化反序列化后的字段填充(如 JsonSerializer 默认跳过 init 属性,除非启用 PropertyNameCaseInsensitive + IgnoreReadOnlyFields = false 等特殊配置)。
init 和 private set 的关键区别:不可变性边界不同
private set 属性允许在类内部任意位置修改,只要在类定义范围内;而 init 属性一旦初始化完成,连类内部也无法再改 —— 这是编译期强制的不可变性保障。
常见误用场景:
- 想在
Init()方法里补全字段?不行,init不认这个方法名 - 在
OnPropertyChanged或事件回调里更新?不行,已过初始化窗口 - 用
MemberwiseClone()复制后改属性?不行,克隆体仍是已初始化状态
如果你需要“创建后仍可内部修改”,就该用 private set;如果目标是“对外不可变 + 初始化语法友好”,才选 init。
required 修饰符必须配合 init 使用,否则无意义
C# 11 引入的 required 仅对 init(或 set)属性生效,它让编译器强制调用方在对象初始化器中提供该属性值:
public class Config
{
public required string Endpoint { get; init; }
public int TimeoutMs { get; init; } = 5000;
}以下代码会编译失败:
var cfg = new Config { TimeoutMs = 3000 }; // ❌ 缺少 required 成员 Endpoint注意几个易漏点:
-
required不能用于只有get的属性(没 setter/init,无法赋值) -
required+init不代表支持默认值回退 —— 没赋值就是编译错误,不会取= null或= default - 若同时存在多个
required属性,必须全部出现在同一对象初始化器中,不能拆到多个语句
init 属性在 JSON 反序列化中默认不工作
Newtonsoft.Json 和 System.Text.Json 默认都**忽略 init 属性**,因为它们依赖反射调用 setter,而 init setter 在运行时没有公开入口(被 [IsExternalInit] 标记)。结果是反序列化后得到全默认值。
解决办法取决于你用的库:
-
System.Text.Json:需启用
JsonSerializerOptions.PropertyNamingPolicy = null并设置IgnoreReadOnlyFields = false(.NET 7+),或升级到 .NET 8 后使用JsonSerializerOptions.IncludeFields = true配合field关键字后备字段 -
Newtonsoft.Json:需添加
[JsonConstructor]到构造函数,并确保所有init属性都在构造参数中映射,或启用JsonPropertyAttribute显式绑定
更稳妥的做法是:DTO 类优先用 record(天然支持 init 反序列化),或明确标注 [JsonInclude] + 启用对应选项 —— 别指望“默认就支持”。
init 的真正价值不在语法糖,而在它把“初始化时机”变成一个可表达、可约束、可验证的语言级概念。容易被忽略的是:它的约束力完全依赖编译器,运行时无额外开销,但一旦跨过初始化边界,连 this 都救不了你。


















