今天聊 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
三个特性加一起,省了多少
拿一个典型的订单服务类举例,三个特性都用上:
这不是个别现象。ERP 项目里三四十个服务类,每个都有构造函数样板。一个类省几行不起眼,乘以四十就多了。
最后给个速查表:
集合表达式
new T[] { ... }主构造器 using 别名
三个特性都不复杂。集合表达式让初始化回归直觉,主构造器干掉了最无聊的样板代码,using 别名让元组终于有了像样的类型名。
一行总结:C# 12 没有加什么颠覆性的东西,它做的事是——把你已经写过一千遍的样板代码,变成不用写了。