首页 / C# 入门教程 / C# 12–14 新特性总览

C# 入门教程

C# 12–14 新特性总览

本教程共 100 篇 · 第 100 篇 · 更新于 2026-07-31 · 约 13 分钟阅读

C#C# 入门教程编程语言C# 12C# 13C# 14扩展成员特性总览

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 集合 / \eC# 13集合与字符串更灵活、更省分配
扩展成员C# 14给已有类型加方法、属性、运算符
空条件赋值C# 14?. 终于能放赋值号左边
nameof 未绑定泛型C# 14nameof(List<>) 干净取泛型名

一、主构造器(C# 12)

C# 12 把「构造参数直接写在类型名后」的主构造器(primary constructor)从 record 扩展到所有 classstruct

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();

参数 nameage 在整个类型体内可见,省掉「字段 + 构造函数」两层样板。第 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 不再局限于数组,可以接受「任意集合表达式支持的集合类型」,包括 SpanReadOnlySpan 和接口:

// 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),一个嵌套对象链能堆出好几层缩进。空条件赋值把「为空就跳过」压成 ?.,阅读负担直线下降。注意「右边不计算」这个细节:若 customernullGetCurrentOrder() 根本不会被调用,这点和 ?. 读取值时一致。

六、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 章。

动手练一练

  1. 用 C# 14 扩展块为 List<int> 写一个扩展属性 Sum,体会扩展成员写法(提示:用 System.LinqSum())。
  2. 写一段空条件赋值:user?.Profile?.Nickname = "小明";,先让 usernull 跑一次,观察赋值被安全跳过。
  3. nameof(List<>)nameof(Dictionary<,>) 打印泛型类型名,对比旧的 nameof(List<int>) 输出有何不同。
上一篇
C# 8–11 重要特性总览
下一篇
已经是最后一篇啦