C# 12–14 新特性总览
本教程共 100 篇 · 第 100 篇 · 更新于 2026-07-31 · 约 13 分钟阅读
100. C# 12–14 新特性总览
本节目标:学完能说出 C# 12–14 各自最亮眼的特性,并亲手写出 C# 14 的扩展成员、空条件赋值与 nameof 未绑定泛型。
这是全书的最后一章,也是「现代 C#」最前沿的一站。C# 12 让类型定义更轻,C# 13 打磨集合与字符串,C# 14(随 .NET 10)则拿出重量级的扩展成员(extension members)。下面按版本走一遍,重点放在 C# 14。
先给一张总表,看清这几版各自的招牌:
| 特性 | 版本 | 一句话价值 |
|---|---|---|
| 主构造器 | C# 12 | 构造参数直接写在类型名后,省字段+构造样板 |
| 集合表达式 | C# 12 | 用 [...] 统一初始化各种集合 |
params 集合 / \e | C# 13 | 集合与字符串更灵活、更省分配 |
| 扩展成员 | C# 14 | 给已有类型加方法、属性、运算符 |
| 空条件赋值 | C# 14 | ?. 终于能放赋值号左边 |
| nameof 未绑定泛型 | C# 14 | nameof(List<>) 干净取泛型名 |
一、主构造器(C# 12)
C# 12 把「构造参数直接写在类型名后」的主构造器(primary constructor)从 record 扩展到所有 class 和 struct:
class Person(string name, int age)
{
public string Name { get; } = name;
public int Age { get; } = age;
public void Greet() => Console.WriteLine($"我叫{Name},{Age}岁");
}
var p = new Person("张三", 18);
p.Greet();
参数 name、age 在整个类型体内可见,省掉「字段 + 构造函数」两层样板。第 91 章已细讲它与 required 的配合,这里记住它是 C# 12 的招牌即可。
为什么这样设计?以前定义一个「带俩字段的类」,要先写私有字段、再写构造函数把参数赋给字段——三处重复同一个名字。主构造器让「参数即字段来源」,名字只写一次。代价是可读性对纯新手略陌生,但习惯后代码明显更短。注意:若只是想「传参初始化」,主构造器配自动属性最舒服;若构造里还要做校验,仍可在体内写常规构造函数重载。
二、集合表达式(C# 12)及其完善(C# 13)
C# 12 的集合表达式(collection expressions)用 [...] 一口气初始化数组、List、跨度(Span)等:
int[] arr = [1, 2, 3];
List<int> list = [1, 2, 3];
Span<int> span = [1, 2, 3];
int[] combined = [1, 2, ..arr, 4]; // .. 是展开运算符,把 arr 拆开拼进去
C# 13 进一步完善:让 params 不再局限于数组,可以接受「任意集合表达式支持的集合类型」,包括 Span、ReadOnlySpan 和接口:
// C# 13:params 可以是 Span,调用时仍传逗号列表,零分配更高效
void Print(params ReadOnlySpan<int> values)
{
foreach (var v in values) Console.Write($"{v} ");
}
Print(1, 2, 3); // 像数组 params 一样调用
Print([4, 5, 6]); // 也能直接传集合表达式
这套组合让「传一串值」既灵活又少分配,是 C# 13 对集合体验的关键补强。第 26 章已专门演示过 [...]。
Tip集合表达式不只是「语法糖」,编译器会按目标类型选最高效的构造方式(比如
Span走栈上分配、接口走对应实现),既好看又不牺牲性能。
三、内插字符串改进(C# 13)
C# 13 给字符串加了新转义序列 \e,代表 ESC 字符(Unicode U+001B),写终端彩色输出时不再拼 \u001b:
// \e 即 ESC;下面拼出一段红色文字(ANSI 转义序列)
string red = "\e[31m红色\e[0m";
Console.WriteLine(red);
同时,C# 13 让对象初始化器里也能用「从末尾数」的 ^ 索引,内插字符串整体在更多场景更顺手。更早的原始字符串字面量(""",C# 11)已在第 99 章介绍过,二者一起让文本处理更从容。
Note内插字符串的「底层 handler」机制在 C# 10 就已引入(如
ToString()用string.Create),C# 13 的\e是面向使用者的直接增强。日常写日志、拼报文会明显受益。
四、扩展成员(C# 14,重点)
这是 C# 14 的头号特性。过去给现有类型加方法,只能用「第一个参数加 this」的扩展方法(extension method)。C# 14 引入扩展块(extension block),可以声明扩展方法、扩展属性、甚至扩展运算符和静态成员,且和旧写法完全兼容。
语法:在顶层、非泛型、静态类里写 extension(接收者类型 接收者名) { ... }:
public static class StringExtensions
{
// 扩展块:为 string 类型的接收者 text 添加成员
extension(string text)
{
// 扩展方法(C# 14 新写法)
public int WordCount() =>
text.Split([' ', '.', '?'], StringSplitOptions.RemoveEmptyEntries).Length;
// 扩展属性
public bool IsEmpty => text.Length == 0;
}
}
// 使用:像实例成员一样调用,无需改 string 源码
string s = "hello world";
Console.WriteLine(s.WordCount()); // 2
Console.WriteLine(s.IsEmpty); // False
要点:
- 接收者名(
text)在块内每个实例成员中都可见;若块里只有静态成员,接收者名可省略。 - 一个静态类里能写多个
extension块,接收不同类型也行。 - 它能声明扩展运算符、静态扩展属性,能力比旧
this写法强。 - 与旧
this参数写法二进制与源码双兼容,可以一个方法一个方法地平滑迁移。
// 旧写法依然有效,且能和新写法并存:
public static class OldStyle
{
public static string Shout(this string s) => s.ToUpper() + "!";
}
Console.WriteLine("hi".Shout()); // HI!
为什么这样设计?旧扩展方法只能加「方法」,加不了「属性」——你想要 s.WordCount 这种像原生属性的写法做不到,只能写成 s.WordCount()。扩展块补齐了这点,还能加运算符和静态成员,让「扩展类型」体验接近「真的给类型加成员」。对比一下:
| 能力 | 旧 this 扩展方法 | C# 14 扩展块 |
|---|---|---|
| 扩展方法 | ✅ | ✅ |
| 扩展属性 | ❌ | ✅ |
| 扩展运算符 | ❌ | ✅ |
| 静态扩展成员 | ❌ | ✅ |
Tip迁移建议:新代码优先考虑扩展块(可读性更好、能写属性);老项目的
this扩展方法不必急着改,两者混用毫无障碍。
Warning扩展属性很方便,但别滥用。调用方在 IDE 里按「点」时,扩展成员和原生成员混在一起出现,若加得太多太偏门,反而让类型显得「什么都能干」、不好预测。给常用、语义清晰的能力加扩展最划算。
五、空条件赋值(C# 14,重点)
?.(空条件成员访问)你早用在「读」上了:person?.Name。C# 14 终于允许把它放在赋值号的左边,形成空条件赋值(null-conditional assignment):
Customer? customer = GetCustomer(); // 可能为 null
// C# 14 之前要这么写:
if (customer is not null)
{
customer.Order = GetCurrentOrder();
}
// C# 14 一行搞定:customer 为 null 时,右边根本不会计算
customer?.Order = GetCurrentOrder();
它同样支持复合赋值,但不支持 ++ / --:
Counter? counter = GetCounter();
counter?.Value += 1; // 复合赋值 OK
// counter?.Value++; // 错误:自增自减不允许
深层也能链式:person?.Address?.City = "北京";——任一环为 null 就整体跳过,不抛异常。这是「防御性编程」最省事的写法。
为什么这样设计?以前给「可能为 null 的对象」赋值,总要包一层 if (x is not null),一个嵌套对象链能堆出好几层缩进。空条件赋值把「为空就跳过」压成 ?.,阅读负担直线下降。注意「右边不计算」这个细节:若 customer 为 null,GetCurrentOrder() 根本不会被调用,这点和 ?. 读取值时一致。
六、nameof 未绑定泛型(C# 14,重点)
nameof 取标识符的名字,常用于日志、抛异常。以前对泛型类型,必须填一个「具体类型参数」才能用:
// C# 14 之前:得硬塞一个类型参数
string old = nameof(List<int>); // "List"
C# 14 允许直接写未绑定(unbound)泛型类型,空 <> 就行:
// C# 14:未绑定泛型,意图更干净
string name = nameof(List<>); // "List"
好处有二:一是不必为了取个名字而捏造一个无关的类型参数;二是和 typeof(List<>) 的写法一致。当你的泛型类日后给类型参数加了约束(比如 where T : class),用 nameof(List<>) 的代码不会因此编译报错,而旧的 nameof(List<int>) 可能要跟着改。写框架、诊断、日志时这个小改进很贴心。
七、C# 14 的其他亮点
除了上面三记重拳,C# 14 还有几条值得一提:
field关键字(正式可用):在自动属性里给单个访问器加逻辑时,用field指代编译器生成的幕后字段,不必手写私有字段。
有了public string Name { get => field; set => field = value ?? throw new ArgumentNullException(nameof(value)); }field,想给属性加「设值时校验」又不想声明私有 backing 字段,一行搞定。- 简单 lambda 参数带修饰符:
TryParse parse = (text, out result) => int.TryParse(text, out result);不必再写全参数类型。 - partial 事件与构造函数:
partial现在能修饰事件和构造函数,方便源生成器(source generator)与手写代码分工。比如自动生成的构造函数在一处、你手写的在另一处,编译时合并。 - Span 一等公民:
Span<T>/ReadOnlySpan<T>获得更多隐式转换,性能代码写起来更自然。
Tip
field关键字和早年的「字段关键字」提案不同,它是 C# 14 才正式落地的能力。用来替代「为了加一行校验就手写_name私有字段」的老习惯,属性体更紧凑。
八、从 8 到 14 的一条线
把最近几版连起来看:C# 一直在「少样板、多表达力、更安全」三条线上同时推进。C# 12 的主构造器与集合表达式砍掉重复代码;C# 13 让集合与字符串更灵活;C# 14 用扩展成员把「给已有类型加能力」这件事做到了前所未有的顺滑,再用空条件赋值和 nameof 未绑定泛型把日常小痛点抹平。
你这 100 章走完,已经掌握了现代 C# 从基础语法到最新特性的主干。接下来靠多写、多读真实项目,把这些语法变成肌肉记忆。
九、本节小结
C# 12 主构造器与集合表达式让定义更轻;C# 13 以 params 集合与 \e 转义补强集合与字符串;C# 14 的扩展成员、空条件赋值、nameof 未绑定泛型则是把「给类型加能力、安全赋值、清晰取名」做到极致。语言在变,目标不变:写出更少、更清楚、更稳的代码。
恭喜你学完《C# 入门教程》全部 100 章。
动手练一练
- 用 C# 14 扩展块为
List<int>写一个扩展属性Sum,体会扩展成员写法(提示:用System.Linq的Sum())。 - 写一段空条件赋值:
user?.Profile?.Nickname = "小明";,先让user为null跑一次,观察赋值被安全跳过。 - 用
nameof(List<>)和nameof(Dictionary<,>)打印泛型类型名,对比旧的nameof(List<int>)输出有何不同。