构造函数是用 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; 显式公开。