Menu

C# 构造函数:默认、带参、this() 与 base() 调用链

C# 构造函数的工作原理:隐式默认构造函数以及它何时消失、带参数和重载的构造函数、用 this(...) 和 base(...) 串联调用、各部分的执行顺序、静态构造函数和私有构造函数,以及 C# 12 的主构造函数。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

构造函数是用 new 创建对象时运行的代码。它与类同名,没有返回类型,它的任务是在任何人使用新对象之前,让它处于有效状态。

输出:

Mug: 8.50
Rejected: price

由于构造函数拒绝负数价格,所以不可能存在价格为负数的 Product。这种保证正是编写构造函数、而不是从外部设置字段的主要原因。

默认构造函数,以及它何时消失

如果一个类没有声明任何构造函数,编译器会提供一个公共的无参构造函数,它除了运行字段初始化器之外什么也不做。这就是为什么只有字段的类也能 new BankAccount()。

一旦你声明了任何构造函数,这个隐式构造函数就不再生成:

class Product
{
    public string Name;
    public Product(string name) { Name = name; }
}

var p = new Product();   // error CS7036: There is no argument given that corresponds to the required parameter 'name'

当序列化器、ORM 或泛型的 new() 约束需要无参构造函数时,这一点常常让人措手不及。如果两种都想要,就自己声明无参构造函数:

public Product() { }                       // or
public Product() : this("Unnamed") { }     // delegate to the other one (next section)

构造函数可以是 public、internal、protected 或 private。它的访问级别决定了谁能用它创建对象。

重载与 this(...) 调用链

一个类可以有多个参数列表不同的构造函数。为了避免在每个构造函数里重复相同的赋值语句,可以把它们串联起来:: this(...) 会在当前函数体运行之前,先调用同一个类的另一个构造函数。

输出:

new Pizza():
  full constructor: medium, classic, 0
  size-only constructor body
  parameterless constructor body
Result: medium classic with 0 toppings

调用链会先运行参数最完整的构造函数,再逐层回到你调用的那个构造函数。把真正的工作(验证、赋值)放在一个构造函数里,其他构造函数只负责提供默认值。

可选参数 是另一种选择:public Pizza(string size = "medium", string crust = "classic", int toppings = 0) 用一个构造函数就覆盖了全部三种调用。不过,当较短的构造函数需要做一些计算,或者修改默认值时不能要求调用方重新编译时,串联调用仍然更合适(可选参数的默认值会在编译时被复制到调用代码中)。

用 base(...) 调用基类构造函数

派生类不会继承构造函数。派生类的每个构造函数都必须先运行基类的某个构造函数。如果你什么都不写,编译器会插入对基类无参构造函数的调用;如果基类没有无参构造函数,你必须用 : base(...) 指定一个。

所有代码的执行顺序值得亲眼看一次:

输出:

Truck field initializer
Vehicle field initializer
Vehicle constructor body
Truck constructor body
KL-204, 3 axles

字段初始化器最先运行,派生类在基类之前;然后构造函数体从基类往下依次运行。所以等到 Truck 的构造函数体运行时,Plate 已经设置好了。这个顺序里有一个陷阱:如果基类构造函数调用了一个被 Truck 重写的 virtual 方法,重写的方法会在 Truck 的构造函数体之前运行,看到的 Axles 仍然是 0。不要在构造函数中调用虚方法。

静态构造函数

静态构造函数初始化的是类型本身,而不是某个对象。它没有参数,也没有访问修饰符,运行时会在第一次使用该类型之前调用它,并且只调用一次。

输出:

Program started
Loading tax table (runs once)
123.00
119.00

运行时保证静态构造函数是线程安全的:即使多个线程同时访问这个类型,它也只运行一次。如果它抛出异常,在整个进程的生命周期中,之后每次使用这个类型都会抛出 TypeInitializationException,所以要让它保持简单,不要包含可能失败的 I/O 操作。

构造函数与对象初始化器

对象初始化器 new Pizza("large") { Toppings = 3 } 并不是第二个构造函数。编译器会把它转换成“先运行构造函数,再给这些成员赋值”。两者各有所长:

  • 构造函数参数用于缺少就会让对象无效的值。每个调用方都必须传入它们,构造函数还能检查它们。
  • 初始化器用于有合理默认值的可选设置。
var order = new Order(customerId: 42) { Note = "Leave at the door", GiftWrap = true };

从 C# 11 开始,标记为 required 的属性必须在初始化器中设置,这让初始化器语法也具备了构造函数的部分保证。这部分内容在属性页面中讲解。

私有构造函数

private 构造函数意味着只有类本身能创建实例。有两种模式会用到它。一种是静态工厂方法,由类来控制创建过程,可以返回缓存的对象或 null;另一种是单例:

输出:

21
100

在类外部写 new Temperature(5m) 时,编译错误是 CS0122,和 访问修饰符 对任何私有成员报的错误相同。具名的工厂方法解决了一个实际的限制:两个构造函数不能都只接收一个 decimal,但 FromCelsius 和 FromFahrenheit 可以。只包含静态成员的类(工具类)应该直接声明为 static,而不是隐藏它的构造函数。

表达式主体构造函数

函数体只有一条语句的构造函数可以使用 =>:

public Product(string name) => Name = name;

借助元组,你甚至可以在一行里给多个字段赋值:public Point(int x, int y) => (X, Y) = (x, y);。

主构造函数(C# 12)

C# 12 允许类或结构体直接在类型声明上写构造函数参数。这些参数在每个成员中都可以使用:

public class Customer(string name, int id)
{
    public string Name { get; } = name;          // copy into a property
    public string Label => $"#{id} {Name}";      // or read a parameter directly

    public Customer(string name) : this(name, 0) { }   // other constructors must chain to it
}

和 record 相比,有两点不同常常让人意外。类的主构造函数参数不会成为公共属性,需要你自己公开。而且在成员中使用的参数会被捕获到一个隐藏字段中,这个字段仍然可变,所以在方法里写 name = "x"; 是能编译通过的。主构造函数最适合依赖注入,此时参数是类只会去调用的服务:

public class OrderService(IOrderRepository repo, ILogger<OrderService> log)
{
    public Order Get(int id) => repo.Find(id);
}

常见错误

  • 丢失了无参构造函数。 添加带参构造函数后,隐式构造函数就没有了(每处 new X() 都会报 CS7036)。
  • 基类没有无参构造函数时忘了写 : base(...)。 编译器无法插入隐式调用,会报告没有为基类构造函数的必需参数提供实参。
  • 在构造函数中调用虚方法。 重写的方法会在派生类构造函数体之前运行。
  • 在构造函数里做繁重的工作。 网络调用、读取文件或任何耗时的操作都会让创建对象变得昂贵,异常也难以处理。这类工作请放到静态工厂方法或异步初始化方法中。
  • 写了返回类型。 public void Product() 是一个名为 Product 的普通方法,编译器会拒绝它,因为成员不能与其所在的类型同名(CS0542)。

常见问题

C# 中的构造函数是什么?

构造函数是用 new 创建对象时运行的特殊方法。它和类同名,没有返回类型:public Product(string name) { Name = name; }。它的任务是让新对象处于有效状态,通常是用参数给字段赋值,并拒绝错误的输入。

C# 会自动创建默认构造函数吗?

只有当类没有声明任何构造函数时才会。这时编译器会添加一个公共的无参构造函数,让每个字段保持其初始值或默认值。一旦你写了任何构造函数,比如一个接收名称的构造函数,这个隐式构造函数就没有了,new Product() 将无法编译(错误 CS7036)。如果仍然需要,请自己声明 public Product() { }。

C# 中如何在一个构造函数里调用另一个构造函数?

在构造函数的参数列表后面写 : this(...):public Product(string name) : this(name, 0m) { }。目标构造函数先运行,然后才运行你调用的那个构造函数的函数体。要调用基类的构造函数,用同样的方式写 : base(...)。

C# 中的静态构造函数是什么?

用 static 标记、没有参数也没有访问修饰符的构造函数,每个类型只运行一次,时间是在创建第一个实例或第一次使用静态成员之前。它用来初始化那些一行初始化语句搞不定的静态字段。你不能自己调用它;如果它抛出异常,这个类型在程序剩余的生命周期里都无法使用(TypeInitializationException)。

应该用构造函数还是对象初始化器?

缺少就会让对象无效的值,用构造函数参数,因为编译器会强制每个调用方传入它们,构造函数还能检查它们。可选设置用对象初始化器(new Product("Mug") { Color = "blue" })。初始化器在构造函数执行完之后才运行。

C# 12 的主构造函数是什么?

直接写在类声明上的参数,class Customer(string name, int id) { ... },类的每个成员都能使用它们。与 record 的位置参数不同,它们不会自动成为公共属性:需要用 public string Name => name; 或 public string Name { get; } = name; 显式公开。

Coddy 编程语言插图

和 Coddy 一起学编程

开始