主构造器与 required 成员
本教程共 100 篇 · 第 91 篇 · 更新于 2026-07-31 · 约 9 分钟阅读
91. 主构造器与 required 成员
本节目标:学完能用主构造器(C# 12)少写样板构造代码,并用 required 成员强制调用方必须赋值。
写类最烦的是什么?多半是字段、属性、构造函数三层重复。一个 Person 类,光把 name 和 age 接进来就得写一遍字段、一遍属性、一遍构造函数。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();
这里 name 和 age 是主构造器参数,生命周期覆盖整个类体。我们只是顺手用它初始化了属性。比起先声明私有字段、再在构造函数里赋值,省了三层代码。
Note主构造器(C# 12)适用于任意
class、struct,不再像早期那样只限于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 { ... })。但你怎么保证调用方一定填了 Id 和 Name?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 的定义。这比运行时才发现 Name 是 null 安全得多。
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();
这条规则保证了:无论用哪个构造入口,主构造器参数都一定被赋值,对象永远处于完整的初始状态。
七、新手踩坑小结
- 普通
class的主构造器参数不是属性,要手动暴露。 record的主构造器参数会自动变成公开属性,别重复声明同名属性。required是编译期强制,漏写就编不过,比运行时判空稳。- 加了主构造器后,隐式无参构造函数就没了;需要无参构造得自己写。
- 一个类里若同时有主构造器和手写构造函数,手写那个必须用
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 怎么选。