Primary Constructors 速查 C# 12 / .NET 8 LTS

class/struct 主构造函数——参数捕获机制、DI 模式、继承规则

Record vs Class — 根本区别

Record(C# 9+)

public record Person(string Name, int Age);
// Name, Age → public init-only 属性 + Deconstruct + Equals…

Class/Struct(C# 12)

public class Person(string name, int age)
{
    public string Name => name;   // 手动暴露
    public int Age => age;
}
// name, age → 不是属性,外部不可见
核心:class 的主构造函数不生成任何属性。参数是"类内全局变量",编译器按需捕获为 private 字段。

捕获语义 Capture Semantics

编译器做逃逸分析——参数出现在哪里决定是否分配字段:

参数被引用的位置示例分配字段?
base(...) / this(...): base(name)❌ 否
仅属性/字段初始化器public string X { get; } = x;❌ 否
表达式体属性 getterpublic int X => x;✅ 是
方法体、实例成员ToString() => name;✅ 是
未被任何成员引用❌ 否 + 警告
关键区分:public string X => x;(表达式体,捕获) vs public string X { get; } = x;(初始化器,不捕获)。前者每次访问都读参数,后者只在构造时读一次存入属性的 BackingField。

捕获的字段 IL 名为 <param>P,不可在 C# 中直接引用。字段是 private 的——即使在继承层次中派生类也访问不到。

依赖注入——最佳场景

// C# 11
public class OrderService : BaseService
{
    private readonly ILogger _logger;
    private readonly IOrderRepo _repo;
    public OrderService(ILogger l, IOrderRepo r) { _logger = l; _repo = r; }
}

// C# 12
public class OrderService(ILogger _logger, IOrderRepo _repo) : BaseService
{
    // _logger, _repo 直接使用——下划线前缀参数名即字段名
}
✅ DI 容器不关心构造函数语法——它只看到参数列表,解析依赖。

构造函数链规则

场景规则
同类的显式构造必须 : this(...) 调用主构造
派生类必须 : base(...) 把参数传给基类
class + 主构造不生成隐式无参构造
struct + 主构造始终有无参构造(CLR 强制)
// 派生类示例——每个类独立捕获
public class Animal(string species)               // Animal 捕获 species
{
    public string Species => species;
}

public class Bird(string species, string name) : Animal(species)
{
    // species 出现在 base(species) 中 → 传给基类
    // species 又出现在 SpeciesName 中 → Bird 自己也要捕获!
    public string SpeciesName => species;
    public string Name => name;
}
// 两个独立的 <species>P 字段——Animal 一个,Bird 一个
陷阱:参数重名时(基类 name + 派生类 name),编译器不报错,但读者要分辨哪个是哪个。建议用不同名字区分。

参数可变性

主构造参数不是 readonly——C# 12/13 都不支持 readonly 修饰。

public class Service(ILogger _logger)
{
    void Bad() { _logger = null; }  // 编译通过!
}
// 传统 DI:readonly 字段 → 编译器保证不可变
// 主构造:全靠纪律——你要 readonly 保证?退回传统写法。

Attribute 目标

// [Obsolete] 放 class 上 → 标记类本身
[Obsolete("Use V2")]
public class OldService(IDb db) { }

// method: 目标 → 标记构造函数本身
[method: Obsolete("Use V2 ctor")]
public class OldService(IDb db) { }

什么时候不该用

快速决策

你的场景用 Primary Constructor?
DI:Service / Controller 注入 2-3 个依赖,透传给基类✅ 最佳
DTO / Model:需要外部可读的属性❌ 用 record
构造函数有校验逻辑❌ 传统构造
struct 数据载体,只需少量暴露✅ 可用