首页 / C# 入门教程 / 主构造器与 required 成员

C# 入门教程

主构造器与 required 成员

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

C#C# 入门教程编程语言主构造器required

91. 主构造器与 required 成员

本节目标:学完能用主构造器(C# 12)少写样板构造代码,并用 required 成员强制调用方必须赋值。

写类最烦的是什么?多半是字段、属性、构造函数三层重复。一个 Person 类,光把 nameage 接进来就得写一遍字段、一遍属性、一遍构造函数。C# 12 的主构造器(primary constructor)和 C# 11 的 required 成员,就是为了解决这个问题来的。

一、主构造器是什么

主构造器把「构造参数」直接写进类名后面。参数在整个类体内都可见,不用再手动存一遍。

namespace CSharpDemo;

class Person(string name, int age)
{
    public string Name { get; } = name;
    public int Age { get; } = age;

    public void Greet() => Console.WriteLine($"我叫{Name},今年{Age}岁。");
}

var p = new Person("张三", 18);
p.Greet();

这里 nameage 是主构造器参数,生命周期覆盖整个类体。我们只是顺手用它初始化了属性。比起先声明私有字段、再在构造函数里赋值,省了三层代码。

Note

主构造器(C# 12)适用于任意 classstruct,不再像早期那样只限于 record。C# 14 仍沿用手写主构造器的写法。也就是说,这是当前稳定的标准能力,放心在日常代码里用。

二、主构造器参数不会自动变成属性

新手最容易踩的坑来了:在普通 class 里,主构造器参数只是参数,不是属性。下面这样写会编译错误——p.name 根本不存在。

class Person(string name, int age) { }

// 错误:name 不是 Person 的公开成员
// Console.WriteLine(new Person("张三", 18).name);

只有在 record 里,主构造器参数才会被自动生成为公开属性:

namespace CSharpDemo;

record Student(int Id, string Name);

var s = new Student(1, "李四");
Console.WriteLine(s.Name); // 自动有 Name 属性
Tip

想让普通 class 也拥有自动属性?那就回到上一节写法,手动 public string Name { get; } = name;。或者干脆用 record,看你的类型是否「以数据为主」。

三、派生类必须调用基类主构造器

一旦基类用了主构造器,派生类在继承时要把参数传上去。编译器不允许你悄悄跳过。

namespace CSharpDemo;

class Point(int x, int y)
{
    public int X { get; } = x;
    public int Y { get; } = y;
}

class ColoredPoint(int x, int y, string color) : Point(x, y)
{
    public string Color { get; } = color;
}

var cp = new ColoredPoint(1, 2, "红");
Console.WriteLine($"({cp.X},{cp.Y}) 颜色 {cp.Color}");

如果你自己又写了带参构造函数,编译器会要求它通过 this() 调用主构造器,确保所有主构造器参数都被赋值。

Warning

加了主构造器后,类就不再有隐式无参构造函数。如果你还需要 new Person() 这种写法,必须自己显式补一个无参构造,并让它链式调用主构造器,例如 public Person() : this("", 0) { }。别等运行时才发现 new 不出来。

四、required 成员:强制调用方赋值

有时候你不想要构造函数,只想用对象初始化器(new Student { ... })。但你怎么保证调用方一定填了 IdName?C# 11 的 required 修饰符就是答案。

namespace CSharpDemo;

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

var s = new Student { Id = 1, Name = "王五" };
Console.WriteLine($"{s.Name} 的分数是 {s.Score}");

漏掉任何一个 required 成员,编译器直接拦下:Student 未包含对必需成员 Id 的定义。这比运行时才发现 Namenull 安全得多。

Note

required 配合 init 访问器最自然:对象一旦建好就不可变,但创建时强制填全。它既能用于属性,也能用于字段(C# 11 起)。如果只想要「必填」又允许后续修改,可改用 set 而非 init

五、两者如何共存

主构造器和 required 可以各管一段。比如主构造器收必填项,required 管可选补充项:

namespace CSharpDemo;

class Order(int id, string customer)
{
    public int Id { get; } = id;
    public string Customer { get; } = customer;
    public required string Address { get; init; }
    public string Note { get; set; } = "";
}

var order = new Order(1001, "张三") { Address = "北京市海淀区" };
Console.WriteLine($"订单 {order.Id}{order.Customer},送到 {order.Address}");

这种写法兼顾「简短」和「强制」。注意顺序:主构造器参数进构造函数,required 成员进对象初始化器,两路各走各的。

六、次级构造函数如何链接主构造器

有时你既想要主构造器,又想提供一个更省事的入口。次级构造函数必须老老实实调用主构造器,不能另起炉灶:

namespace CSharpDemo;

class Person(string name, int age)
{
    public string Name { get; } = name;
    public int Age { get; } = age;

    // 次级构造必须链式调用主构造器
    public Person(string name) : this(name, 0) { }

    public void Greet() => Console.WriteLine($"我叫{Name}{Age}岁");
}

var p1 = new Person("张三", 18);
var p2 = new Person("李四"); // 年龄默认 0
p1.Greet();
p2.Greet();

这条规则保证了:无论用哪个构造入口,主构造器参数都一定被赋值,对象永远处于完整的初始状态。

七、新手踩坑小结

  1. 普通 class 的主构造器参数不是属性,要手动暴露。
  2. record 的主构造器参数会自动变成公开属性,别重复声明同名属性。
  3. required 是编译期强制,漏写就编不过,比运行时判空稳。
  4. 加了主构造器后,隐式无参构造函数就没了;需要无参构造得自己写。
  5. 一个类里若同时有主构造器和手写构造函数,手写那个必须用 this(...) 调主构造器,否则编译不过。
Tip

到底用哪种?数据为主、要值相等性的用 record;要行为、要继承的用 class + 主构造器;想强制初始化又不要长构造函数签名的,用 required 成员。三者不冲突,按场景组合。

八、为什么这样设计

C# 历来被吐槽「仪式感太重」:一个只装两个字段的数据类,也得写字段、属性、构造函数一大串,真正的业务逻辑反而被淹没了。主构造器把「构造参数」提到类名旁,参数在类体内随处可用,等于让编译器替你生成了那段样板。

required 则补上了对象初始化器的短板。对象初始化器写起来舒服,但编译器原本无法要求「必须填谁」。加上 required 后,缺一个都过不了编译,把「运行时才可能发现的漏填」提前到「写代码时就被拦下」。一句话:两个特性都服务于同一个目标——用编译器的强制力,换来更少代码、更少隐藏 bug。

九、与其它写法对比

写法何时用缺点
传统构造函数需要复杂初始化逻辑字段/属性/构造三层重复
主构造器构造参数直接喂给属性/字段参数默认不是公开属性
对象初始化器字段多、想逐个赋值无法强制必填
required 成员要强制必填又不愿写长构造只能用于成员,不能用于局部变量
record数据为主、要值相等性偏「数据载体」,不太适合放重行为

简单说:比谁更省事、更安全就选谁,但别为了炫特性牺牲清晰。

Tip

主构造器参数也能带默认值,例如 class Person(string name, int age = 18)。这样调用方可以省略年龄,直接 new Person("张三")。配合 required 成员,你能灵活组合「必填构造参数」与「可选对象初始化项」,既保持简洁又不失强制力。记住一条原则:用编译器逼出完整初始化,而不是靠运行时判空去兜底。

十、本节小结

主构造器把构造参数提到类名后,减少样板;required 成员用编译期检查逼出完整初始化。两个特性都服务于同一个目标:更少代码、更少隐藏 bug。下一章我们看 struct 与 class 怎么选。