×

C# 12 这 3 个特性,写完代码量少一半

独孤求败 独孤求败 发表于2026-09-28 16:52:12 浏览40 评论0

抢沙发发表评论

今天聊 C# 12 的三个新特性。集合表达式、主构造器、using 别名改进——三个东西看着不起眼,但真用上之后,样板代码一行不剩。

先说为什么要折腾这些

上周重构秦巴牧云的一个模块,新建了六七个服务类。写完一看,光构造函数和字段赋值就占了快六十行。

我数了一下,一个标准的依赖注入服务类,十行代码里八行是模板:声明字段、写构造函数、参数赋值。真正干活的逻辑就两行。

然后翻了翻 C# 12 的文档,试了三个特性。同样那几个类重写了一遍,代码量直接砍了一半多。

01

集合表达式:初始化列表不再写 new

C# 12 之前,初始化一个集合,你得写完整类型名 + new 关键字。数组也一样。

C# 12 之后,方括号搞定一切。编译器根据左边类型自动推断。

改造前

// 初始化数组
int[] roles = new int[] { 1, 2, 3 };

// 初始化 List
List<string> permissions = new List<string>
{
    "read",
    "write",
    "delete"
};

// 合并两个列表 + 追加一个
var allPerms = new List<string>();
allPerms.AddRange(defaultPerms);
allPerms.AddRange(extraPerms);
allPerms.Add("admin");

改造后

// 初始化数组
int[] roles = [1, 2, 3];

// 初始化 List
List<string> permissions = ["read", "write", "delete"];

// spread 操作符 ..  合并 + 追加
List<string> allPerms = [..defaultPerms, ..extraPerms, "admin"];

最后那段合并列表最明显。原来三行做的事,现在一行搞定。两个点 .. 是 spread 操作符,把一个集合展开塞进新集合里。和 JS 的 ... 一个意思。

秦巴牧云的多租户系统里,每个租户初始化时要塞一组默认角色权限。以前用 new List<string> { ... },三十多个租户种子数据,光是花括号就看得眼花。现在全换成了方括号。

有一点要注意:[1, 2, 3] 是编译期生成的,不是运行时调用 new List<T>()。所以它支持数组、Span<T>、ImmutableArray<T> 等多种目标类型——编译器看左边的类型决定怎么生成。

02

主构造器:干掉构造函数样板

这是三个特性里省代码最多的一个。

一个依赖注入的服务类,C# 12 之前的写法:声明三个私有字段,写一个构造函数,三行赋值。真正的业务逻辑藏在最后面。

改造前

public class OrderService
{
    private readonly IOrderRepository _orderRepo;
    private readonly ILogger<OrderService> _logger;
    private readonly ITenantProvider _tenant;

    public OrderService(
        IOrderRepository orderRepo,
        ILogger<OrderService> logger,
        ITenantProvider tenant)
    {
        _orderRepo = orderRepo;
        _logger = logger;
        _tenant = tenant;
    }

    public async Task<Order> GetOrder(int id)
    {
        var order = await _orderRepo.GetByIdAsync(id);
        _logger.LogInformation("GetOrder {Id}", id);
        return order;
    }
}

改造后

public class OrderService(
    IOrderRepository orderRepo,
    ILogger<OrderService> logger,
    ITenantProvider tenant)
{
    public async Task<Order> GetOrder(int id)
    {
        var order = await orderRepo.GetByIdAsync(id);
        logger.LogInformation("GetOrder {Id}", id);
        return order;
    }
}

类名后面直接跟参数列表,构造函数、字段声明、赋值三件事一步到位。参数在整个类体内都能直接用——不需要 this. 前缀。

ERP 项目里有三十多个服务类,一个一个改过来。粗算了一下,每个类省 6 到 8 行,总共省了大概两百多行。两百行样板代码,全是构造函数。

但有一个坑你得知道。

主构造器的参数不是字段。它更像方法参数——在类体内可见,但不会自动存为实例字段。这意味着:

// 如果后续需要以字段形式持有(比如被其他方法间接引用)
// 需要手动捕获:
public class OrderService(IOrderRepository orderRepo, ...)
{
    private readonly IOrderRepository _repo = orderRepo;
    //                              ^^^^^^^^^^^^^^^^^^ 手动捕获
}

简单说:只在当前类的方法里直接用,不捕获也行;但如果要传给其他对象或被闭包引用,编译器会帮你生成字段。行为没问题,就是别以为它天生就是 readonly 字段。

03

using 别名改进:元组也能起名字

C# 12 之前,using 别名只能指向命名类型——类、结构体、接口。元组这种"匿名组合"类型不能起别名。

所以你要么手写一个 struct,要么忍受 Item1 / Item2 这种谁也看不懂的属性名。

改造前

// 要语义化坐标?手写 struct
public readonly struct Point
{
    public int X { get; }
    public int Y { get; }
    public Point(int x, int y) { X = x; Y = y; }
}

// 或者直接用元组,但属性名是 Item1/Item2
var p = (10, 20);
var x = p.Item1; // 谁知道这是啥
var y = p.Item2;

改造后

// 文件顶部直接起别名
using Point = (int X, int Y);
using Size = (int Width, int Height);

// 用起来就跟普通类型一样
Point origin = (0, 0);
Point dest = (10, 20);

int dx = dest.X - origin.X;  // 10
int dy = dest.Y - origin.Y;  // 20

六个字母定义一个语义化类型,不用写 struct、不用写构造函数、不用写属性。元组的字段名(X、Y)直接成为类型的一部分,编译器认。调代码时 dest.X 比 dest.Item1 可读性高一个数量级。

HyxPcbERPNew 里有不少坐标和尺寸的场景——板料的长宽、拼板偏移量。以前都是 (int, int) 硬写,注释写"前面是 X 后面是 Y"。现在别名一加,类型自己就会说话。

04

三个特性加一起,省了多少

拿一个典型的订单服务类举例,三个特性都用上:

改造前 vs 改造后(代码行数)改造前改造后0102030405060集合初始化12 行3 行构造函数样板16 行8 行坐标类型定义6 行1 行合计:34 行 → 12 行,砍掉 65%

这不是个别现象。ERP 项目里三四十个服务类,每个都有构造函数样板。一个类省几行不起眼,乘以四十就多了。

最后给个速查表:

特性
替代了什么
省多少
注意
集合表达式
new List<T> { ... }
new T[] { ... }
~75%
编译期生成,支持 spread
主构造器
字段声明 + 构造函数 + 赋值
~50%
参数非字段,需手动捕获
using 别名
手写 struct/class
~83%
值类型语义,注意装箱


三个特性都不复杂。集合表达式让初始化回归直觉,主构造器干掉了最无聊的样板代码,using 别名让元组终于有了像样的类型名。

一行总结:C# 12 没有加什么颠覆性的东西,它做的事是——把你已经写过一千遍的样板代码,变成不用写了。


群贤毕至

访客