首页 / C# 入门教程 / 属性进阶(init/required)

C# 入门教程

属性进阶(init/required)

本教程共 100 篇 · 第 35 篇 · 更新于 2026-07-31 · 约 12 分钟阅读

C#C# 入门教程initrequired属性不可变

35. 属性进阶(init / required)

本节目标:学完你能写出「创建时必须给值、创建后不可改」的健壮对象。

上一章的只读属性 { get; } 很安全,但只能在构造函数里赋值,用对象初始化器写不了。C# 9 的 init 和 C# 11 的 required 把这件事做得更顺手,也更适合不可变(immutable)数据。

init 访问器:初始化时可写,之后冻结

set 换成 init,属性就能在对象初始化器里赋值,但对象建好后再赋值会编译报错。它实现了「一次性写入、之后只读」。

namespace CSharpDemo;

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

var p = new Person { Name = "小兰", Age = 20 };
// p.Age = 21;   // 报错:init 属性创建后不可改
Note

initset 的区别只在时机:set 随时可改,init 只在对象初始化期间可写。它正是 record 不可变语义的底层支撑(record 见第 49 章)。

required 成员:不给值就编译报错

有时候某个属性「必须填」,漏了就是 bug。C# 11 的 required 修饰符强制调用方在初始化器里提供该成员,否则直接编译失败,把错误挡在最早。

namespace CSharpDemo;

class Student
{
    public required string Name { get; init; }
    public int Score { get; set; }
}

// var s = new Student { Score = 90 };   // 报错:缺少必需的 Name
var s = new Student { Name = "小伟", Score = 90 };

required 可配合 init,也可配合普通 set。它管的是「有没有赋值」,不管值本身合不合法。

Tip

required + init 是黄金组合:既强制必填,又保证创建后不可篡改,最适合表示「身份类」数据,比如订单号、用户名。

给 init 属性做校验

init 访问器里能写逻辑,所以可以在赋值时顺手校验。下面要求名字不能为空,否则抛异常——因为初始化阶段就会执行 init,错误立刻暴露。

namespace CSharpDemo;

class Person
{
    private string name;
    public string Name
    {
        get => name;
        init
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new ArgumentException("名字不能为空");
            name = value;
        }
    }
}

var p = new Person { Name = "阿杰" };      // 正常
// var q = new Person { Name = "  " };     // 抛异常
Note

校验写在 init 里,而不是等对象用完才发现问题,这正是「失败得越早越好」的体现。

required 与可空类型的配合

开启了可空引用类型(string?)时,required string? 表示「必须出现、但允许是 null」;若想连 null 都不允许,用非可空的 required string 配合上面的非空校验最稳。

namespace CSharpDemo;

class Product
{
    public required string Name { get; init; }   // 必填且不可为 null
    public decimal Price { get; init; } = 9.99m; // 可选,有默认值
}

var p = new Product { Name = "水杯" };
Console.WriteLine($"{p.Name}{p.Price}");   // 水杯 价 9.99

Price 没标 required,又有默认初值,所以可省略;Name 必须给,不给就编译不过。

init / required 解决了什么痛点

在没有这两个特性前,想做「必填 + 不可变」只能靠构造函数参数堆砌。字段一多,构造函数签名就长得吓人。init + required 让你用初始化器就能表达同样约束,代码短、可读性高。

Tip

新手踩坑:在 init 属性上同时想「外部创建后还能改一点」,这是不可能的——init 就是为不可变而生的。需要可改就回到 set

为什么需要不可变

对象创建后数据就不再变,会带来三个好处:一是可预测,任何时刻看它都一样;二是线程安全,多线程读同一对象不会读到半成品;三是稳定,若拿它当字典的键或算哈希,值不变才不会出乱子。不可变不是炫技,是少 bug 的利器。

Note

init 正是为「创建时定好、之后不变」量身定做的。它比构造函数参数更灵活——你可以只给需要的部分,其余走默认。

init 与 record 的关系

本书第 49 章会讲 record(记录类型)。记录的属性默认就是 init 的,这正是它「不可变 + 值语义」的底层来源。理解 init,就等于提前理解了 record 的一半。

namespace CSharpDemo;

record Point(int X, int Y);   // 位置记录,X/Y 本质是 init 属性
var p = new Point(1, 2);
// p.X = 3;   // 报错:record 的属性是 init

多个 required 成员一起用

一个类可以有多个 required 成员,调用方必须全部补齐,缺一不可。初始化器写起来依然直观。

namespace CSharpDemo;

class User
{
    public required string UserName { get; init; }
    public required string Email { get; init; }
    public string Nickname { get; init; } = "匿名";
}

var u = new User { UserName = "alice", Email = "a@x.com" };
Console.WriteLine($"{u.UserName} <{u.Email}>");

required 与主构造器怎么选

C# 12 的主构造器也能强制必填(把参数写在类声明上)。两者的取向不同:required 配合初始化器,调用方可以「挑着填、其余用默认」;主构造器则强制按固定参数顺序传入。字段多且多数可选时,required 往往更舒服。

Tip

常见坑:required string? 只要求「出现」,仍可能赋 null。想连 null 都不允许,用非可空的 required string 并加非空校验,双保险。

同一个类里混合 init 与 set

一个类完全可以有的属性用 init(创建后不可改),有的用 set(随时可改)。这样既能锁定身份数据,又保留可变的业务状态。

namespace CSharpDemo;

class Order
{
    public required int Id { get; init; }        // 订单号:定死
    public string Status { get; set; } = "新建";  // 状态:可改
}

var o = new Order { Id = 1001 };
o.Status = "已付款";
// o.Id = 2002;   // 报错:init 不可改

required 配合集合属性

required 也能用在集合属性上,强制调用方至少给一个初始集合。结合 C# 12 集合表达式,初始化很简洁。

namespace CSharpDemo;

class Config
{
    public required List<string> Roles { get; init; }
}

var c = new Config { Roles = ["admin", "user"] };
Console.WriteLine(string.Join(",", c.Roles));
Note

集合表达式 ["admin", "user"] 是 C# 12 语法,详见第 26 章。它让 required 集合的初始化不再啰嗦。

用 required 表达「配置对象」

做配置类时,关键项用 required 强制填,可选项给默认值,调用方一眼就知道哪些不能漏。这比靠文档叮嘱靠谱得多——漏了编译器直接拦下。

Tip

常见坑:required 只保证「写了这个成员」,不保证「写了合法值」。涉及非空、范围等约束,仍要在 init 里做校验,双保险才稳。

init 在结构(struct)上同样可用

init 不只用于类,值类型 struct 也能用。下面这个只读点结构创建后可初始化、之后不可改,适合做轻量不可变值。

namespace CSharpDemo;

struct Point
{
    public int X { get; init; }
    public int Y { get; init; }
}

var p = new Point { X = 1, Y = 2 };
// p.X = 3;   // 报错:init 不可改

required 与可空引用的细节

开启可空引用类型后,required stringrequired string? 语义不同:前者要求调用方给一个非空的字符串;后者只要求「出现这个成员」,值仍可为 null。若要严格防 null,优先用非可空版本并配合非空校验。

Note

可空分析只在编译期提醒,运行时不会自动拦截 null。所以「既 required 又非空」需要 required string 加上 init/set 里的判空,双重保险。

init 与对象初始化器的顺序

init 访问器在对象初始化器执行期间逐个被调用,全部完成后对象才「冻结」。因此多个 init 属性之间可以互相独立赋值,不必担心初始化中途被外界读到半成品——因为对象还没真正交付使用。

Tip

想让一组属性「要么全给、要么用默认」,就把它们都设成 init 并给默认值,调用方只填关心的那几个即可。

小结

init 访问器允许在对象初始化阶段写入、之后冻结,是构建不可变类型的利器,类与 struct 都可用,也是 record 的底层支撑。required 强制调用方在初始化器里提供成员,漏写直接编译报错。两者常组合成 required ... init,既必填又不可改。一个类里可混合 initset:身份数据用 init 锁死,业务状态用 set 可变。init 里还能写校验逻辑,把非法数据挡在创建那一刻。可空类型下,required stringrequired string? 更严格、更防 null