导读:本期聚焦于厦门程序员创作的《C# init访问器是什么?详解C# 9.0只读属性初始化的正确用法》,敬请观看详情。为什么同一个属性,既能在外部初始化时赋值,又能在后续使用中保持只读?这正是C# 9.0引入的init访问器要解决的问题。本文围绕init关键字展开,先讲清楚它与set访问器在编译器层面的差异,再通过对象初始化器、record类型、构造函数配合等典型场景演示具体写法,同时分析init-only属性与readonly字段的本质区别、常见的编译报错原因,以及老版本.NET项目中调用含init属性类库时的兼容性处理方案。读完这篇内容,你可以准确判断什么时候该用init代替set,避免属性被意外修改带来的隐蔽bug。

C# 9.0 引入了一个很有意思的语言特性——init 访问器。它允许属性只在对象初始化阶段被赋值,初始化完成之后再修改就会直接报编译错误。很多刚接触这个特性的开发者会疑惑:它和传统的 set 访问器有什么区别?和 readonly 字段又是什么关系?这篇文章把这些问题一次讲透。

C# init访问器是什么?详解C# 9.0只读属性初始化的正确用法

init 访问器的基本概念与底层原理

先看最直观的对比。传统写法中,属性一旦定义了 set 访问器,就意味着它在对象生命周期的任何时刻都可以被修改:

public class Person
{
    public string Name { get; set; }
}

这种写法的问题在于,如果设计意图是“Name 一旦确定就不该变”,set 访问器无法在语言层面约束这一点,只能靠开发者自觉。而把 set 换成 init 之后,约束就成了编译器强制的规则:

public class Person
{
    public string Name { get; init; }
}

var p = new Person { Name = "张三" };  // 正确,初始化阶段
// p.Name = "李四";                    // 编译错误,初始化已完成

从编译器实现的角度看,init 并不是把属性变成真正的只读。编译器会在构造对象时通过一个特殊的 modreq(IsExternalInit 类型修饰符)标记 set 访问器,使它只能在对象初始化器或构造函数中被调用。也就是说,IL 层面它仍然是一个 setter,只是被标记为“仅限初始化”。理解这一点很重要,后面讲跨版本兼容时会用到。

init 的典型使用场景与代码示例

init 最常见的用法是配合对象初始化器,构建不可变的数据对象。相比在构造函数里传一堆参数,对象初始化器的写法更清晰,参数顺序也不会出错:

public class Order
{
    public int OrderId { get; init; }
    public string CustomerName { get; init; }
    public DateTime CreatedAt { get; init; }
}

var order = new Order
{
    OrderId = 1001,
    CustomerName = "王五",
    CreatedAt = DateTime.Now
};

第二个典型场景是 record 类型。C# 9.0 的 record 本身就是为不可变数据设计的,它自动生成的主构造函数参数默认就是 init-only 属性。如果你手写一个简单的不可变 DTO,用 class 加 init 属性和用 record 效果接近,但 record 额外提供了基于值的相等性比较和 with 表达式支持。

第三个场景是在构造函数中赋值。init-only 属性允许在所在类的构造函数内部直接赋值,这一点和对象初始化器效果一致:

public class Config
{
    public string Endpoint { get; init; }

    public Config(string endpoint)
    {
        Endpoint = endpoint;  // 构造函数内赋值,合法
    }
}

需要注意,只有在定义该属性的类自己的构造函数里才能这样写。如果你在别的类里 new 完对象再赋值,编译器会直接拒绝。

init 与 readonly、set 的区别及常见坑

很多人分不清 init-only 属性和 readonly 字段。两者的核心区别在于初始化方式的灵活性:readonly 字段只能在声明时、所在类的构造函数中赋值,无法通过对象初始化器赋值;而 init 属性可以。下面的代码能直观看出差异:

public class Sample
{
    public readonly int A = 1;          // 声明时赋值
    public int B { get; init; }

    public Sample(int a) { A = a; }     // 构造函数赋值
}

// var s = new Sample(5) { A = 10 };    // 错误,readonly 不能用初始化器
var s = new Sample(5) { B = 10 };       // 正确

再说说几个常见的坑。第一个坑是和 init 配合的自动属性必须使用简写形式,如果你手写完整的访问器体并加 init,在某些早期预览版本会有语法限制,正式版已经支持,但建议保持简写风格让代码更整洁。

第二个坑是跨版本兼容问题。因为 init 依赖 IsExternalInit 类型,在 .NET 5 及以上版本开箱即用,但如果你的类库需要被 .NET Framework 或旧版 .NET Standard 项目引用,消费方编译时会报缺少类型的错误。解决办法是在类库项目中手动声明一个内部命名空间兼容类型:

namespace System.Runtime.CompilerServices
{
    // 为旧运行时补上 init 所需的类型标记
    internal static class IsExternalInit
    {
    }
}

加上这个声明后,旧框架项目也能正常编译引用含 init 属性的类库,这是类库作者必须掌握的兼容技巧。

第三个坑和反序列化有关。JSON 反序列化器需要能写入属性才能完成绑定,主流库如 System.Text.Json 和 Newtonsoft.Json 的新版本都已经支持 init-only 属性的写入,但如果项目还停留在很旧的序列化库版本,反序列化含 init 属性的类型可能失败或得到默认值,升级序列化库即可解决。

什么时候该用 init,什么时候不该用

判断标准其实很简单:如果某个属性在对象创建后 logically 不应该再变化,就大胆用 init。典型的有配置项、订单编号、创建时间、身份标识这类“出生即定”的值。反之,像购物车数量、用户昵称这类会随业务流转而修改的状态,用普通 set 才是正确选择。

另外提醒一点,init 提供的是编译期保护,不是运行时安全机制。通过反射依然可以在初始化之后修改 init-only 属性的值,所以不要把它当作防止恶意篡改的手段,它约束的是正常的代码编写行为,让意图在类型签名上表达清楚,这本身已经能消除大量隐蔽的赋值 bug 了。

C# init访问器C# 9.0只读属性修改时间:2026-09-10 21:53:05

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0910/54290.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。