下面是我整理的一份 C# 14(随 .NET 10 推出)新特性实用指南,包括每个特性的动机、用法、注意事项,以及示例。你可以把它当作快速参考或学习笔记。

⚠️ 前提说明

  • 目前很多特性仍处于预览阶段,语法或行为可能在最终版本中有调整。
  • 要启用这些特性,你的项目可能需要设置 <LangVersion>preview</LangVersion> 或明确指定 C# 14。
  • 部分特性在已有代码中可能引入语法歧义或破坏性变更,需要审慎采用。

一、C# 14 的主要新特性汇总

根据 Microsoft 官方 “What’s new in C# 14” 文档,以下是 C# 14 的主要新特性:

  • 扩展成员(Extension Members) (微软学习)
  • null 条件赋值(Null-Conditional Assignment) (微软学习)
  • nameof 支持未绑定泛型类型(unbound generic types) (微软学习)
  • Span<T> / ReadOnlySpan<T> 的更多隐式转换支持 (微软学习)
  • 简化 Lambda 参数上的修饰符(Modifiers on simple lambda parameters) (微软学习)
  • 字段支持属性 (“field backed properties”) (微软学习)
  • partial 构造函数和 partial 事件 (微软学习)
  • 用户定义的复合赋值运算符(User-defined compound assignment operators) (微软学习)
  • 运行单个 C# 源文件(File-based apps / 脚本式运行) (InfoWorld)

下面我逐个拆开说。


二、各新特性详解与示例

1. 扩展成员(Extension Members)

动机 / 意义
C# 的扩展方法一直是扩展类型行为的重要机制,但它只能扩展方法(静态方法形式)而不能直接扩展属性、或静态成员。C# 14 引入扩展成员语法,使得可以扩展属性、静态方法/属性,以及在新的语法范式下组织扩展。

语法与用法

public static class MyExtensions
{
    extension&lt;T>(IEnumerable&lt;T> source)  // 为 IEnumerable&lt;T> 定义扩展成员
    {
        // 扩展属性(read-only)
        public bool IsEmpty => !source.Any();

        // 扩展方法
        public IEnumerable&lt;T> WhereGreaterThan(T threshold)
            => source.Where(x => Comparer&lt;T>.Default.Compare(x, threshold) > 0);
    }

    extension&lt;T>(IEnumerable&lt;T>)  // 定义静态扩展成员(不针对实例)
    {
        public static IEnumerable&lt;T> Combine(IEnumerable&lt;T> a, IEnumerable&lt;T> b)
            => a.Concat(b);

        public static IEnumerable&lt;T> Identity => Enumerable.Empty&lt;T>();

        public static IEnumerable&lt;T> operator +(IEnumerable&lt;T> a, IEnumerable&lt;T> b)
            => a.Concat(b);
    }
}

  • 第一个 extension<T>(IEnumerable<T> source) 块定义“实例级扩展成员” —— 调用时像普通成员一样: myList.IsEmpty
  • 第二个 extension<T>(IEnumerable<T>) 块定义“静态扩展成员” —— 调用时像类型成员一样: IEnumerable<int>.IdentityIEnumerable<int>.Combine(a, b)
  • 在这个语法中,也可以定义运算符重载扩展成员。 (Microsoft for Developers)
  • 已有的 this-parameter 扩展方法仍然可以继续使用,两种语法共存。 (Microsoft for Developers)

注意 / 限制

  • 扩展成员仍是静态方法在底层被调用(编译器会“lower”到普通静态方法)。 (Microsoft for Developers)
  • 并不是所有 this-parameter 扩展方法都能直接迁移为扩展成员语法(某些复杂泛型签名会受限)。 (Microsoft for Developers)
  • 如果在类型中已有名为 fieldextension 或其他关键字的成员,需要用 @ 前缀绕开命名冲突。
  • 此特性目前仍为预览状态。

示例使用

var list = new List&lt;int> { 1, 2, 3 };
if (list.IsEmpty)
{
    Console.WriteLine("Empty");
}

var combined = IEnumerable&lt;int>.Combine(new[] { 1, 2 }, new[] { 3, 4 });


2. Null 条件赋值(Null-Conditional Assignment)

动机 / 意义
在以前的 C# 版本中,?. 运算符只能用于 读取 操作(safe navigation / null conditional),不能用于赋值。许多代码需要先判断对象是否为 null,再执行赋值语句。C# 14 扩展了这个能力,使得赋值也可以用 null 条件方式写,从而简化代码。

语法与用法

  • 可以这样写: customer?.Order = newOrder; 只有当 customer 不为 null 时,才会执行 customer.Order = newOrder。如果 customer 为 null,则赋值被跳过。(Ivan Kahl’s Blog)
  • 支持复合赋值(如 +=, -= 等): results?.Count += 5; 只有在 results 不为 null 时,才会对 Count 加 5。(Ivan Kahl’s Blog)
  • 也可以用于索引器(indexer): dict?["Key"] = value;
  • 限制不支持 ++-- 运算符的 null 条件形式。也就是说,下面这种写法不合法: customer?.TotalOrders++; // ❌ 编译错误 要做到类似效果,仍需要改写为 customer?.TotalOrders += 1 或用传统判断。(Ivan Kahl’s Blog)

示例

// 老写法
if (config != null &amp;&amp; config.Settings != null)
    config.Settings.RetryPolicy = rp;

// 新写法(C# 14)
config?.Settings?.RetryPolicy = rp;

// 复合赋值例子
results?.ItemsProcessed += 10;

// 索引器赋值
customerData?["LastLogin"] = DateTime.UtcNow;


3. nameof 支持未绑定泛型类型(Unbound Generic Types)

动机 / 意义
以前 nameof 只能用于闭合类型(如 List<int>),不能写 nameof(List<>)。但在代码生成、反射、日志等场景中,有时我们只关心类型名(如 “List”),而不关心具体泛型参数。C# 14 拓宽了这一能力。

语法与用法

Console.WriteLine(nameof(List&lt;>));        // 输出 "List"
Console.WriteLine(nameof(Dictionary&lt;,>));  // 输出 "Dictionary"

示例

string s1 = nameof(List&lt;>);         // "List"
string s2 = nameof(Dictionary&lt;,>);   // "Dictionary"

相比以前只能写:

string s = nameof(List&lt;int>);  // "List"

现在更灵活一些。(微软学习)


4. 对 Span<T> / ReadOnlySpan<T> 的更多隐式转换支持

动机 / 意义
Span<T>ReadOnlySpan<T> 是用于高性能内存操作的重要类型,广泛用于处理切片 (slicing)、内存视图、零分配操作等。在之前的 C# 版本中,很多操作需要显式转换或包装。C# 14 加强了语言一体化支持,使得 Span 更加自然地融入语言。

语法与用法

  • 新增隐式转换支持,让 T[]Span<T>ReadOnlySpan<T> 之间的转换更加自然。(微软学习)
  • 支持将 Span<T> / ReadOnlySpan<T> 作为扩展方法的接收器(receiver)。(c-sharpcorner.com)
  • 在类型推断 (generic inference) 和组合转换场景中更智能。例如可以在更复杂的泛型环境下减少显式转换。

示例(假想):

Span&lt;int> span = new int[] { 1, 2, 3 };         // 隐式从数组转换
ReadOnlySpan&lt;int> roSpan = span;                // 隐式转换
int[] arr = span;                               // 隐式转换回数组(若安全)
span.SomeExtensionMethod();                     // span 作为接收器使用扩展方法

注意 / 风险

  • 新的隐式转换可能会引发重载解析上的歧义。代码升级时要注意可能的重载优先级变化。
  • 即便有这些隐式支持,仍要留意性能、边界检查等问题。

5. 简化 Lambda 参数上的修饰符(Modifiers on Simple Lambda Parameters)

动机 / 意义
在以前版本中,如果 Lambda 参数带有 refinoutref readonly 等修饰符,就必须为参数写出类型。不能用简化形态。C# 14 放宽这个限制,让这些修饰符可以直接写在简化 Lambda 语法上,从而减少冗余。

语法与用法

例如:

delegate bool TryParse&lt;T>(string text, out T result);

// 以前必须写完整参数类型
TryParse&lt;int> p1 = (string text, out int result) => Int32.TryParse(text, out result);

// C# 14 允许省略类型,同时加修饰符
TryParse&lt;int> p2 = (text, out result) => Int32.TryParse(text, out result);

// 甚至可以有 in/ref 等修饰符
Func&lt;ReadOnlySpan&lt;char>, bool> trySpan = (scoped span) => DoSomething(span);

注意 params 修饰符在这种简化语法中仍需要显式类型声明。 (微软学习)


6. 字段支持属性 (“field backed properties” / field 关键字)

动机 / 意义
在过去,如果希望在属性(Property)访问器(getter/setter)里写自定义逻辑(如验证、异常抛出),通常就要声明一个私有字段作为 backing field,再在 get / set 中操作。这样有些繁琐。C# 14 引入 field 关键字,使得在自动属性的访问器里可以直接引用编译器合成的 backing field,从而减少样板代码。

语法与用法

public class MyClass
{
    public string Name
    {
        get;
        set => field = value ?? throw new ArgumentNullException(nameof(value));
    }
}

这个写法中:

  • field 是编译器合成的 backing field,你无需自己定义 _name
  • get; 是自动实现 getter;set 中写自定义逻辑。
  • 你也可以为 get 添加逻辑、或两侧都写逻辑。

示例

public int Age
{
    get;
    set
    {
        if (value &lt; 0) throw new ArgumentOutOfRangeException(nameof(value));
        field = value;
    }
}

注意 / 限制

  • 如果你的类中已有成员名叫 field,可能会冲突,此时可以用 @fieldthis.field 区别。
  • 这个特性在早期版本(C# 13)已作为预览功能出现。 (微软学习)
  • 在非常复杂的继承/反射场景下要慎用,以免引入混淆。

7. partial 构造函数与 partial 事件

动机 / 意义
C# 的 partial 类型早有(让类/结构/接口拆分在多个文件中定义),但以前 partial 的能力局限于方法、属性、索引器等。C# 14 拓展 partial 到构造函数和事件,使得你可以在不同文件中拆分构造逻辑或事件实现,实现模块化或代码生成时更灵活拆分。

语法与用法

  • 对于构造函数: public partial class MyClass { public partial MyClass(); } // 在另一个文件中实现 public partial class MyClass { public partial MyClass() { // 构造逻辑 } } 实现声明(implementing declaration)可以带 this()base() 初始化器。 (微软学习)
  • 对于事件: public partial class MyClass { public partial event EventHandler MyEvent; } // 在另一个文件中 public partial class MyClass { public partial event EventHandler MyEvent { add { … } remove { … } } } 定义声明通常是 field-like 事件,实际 add/remove 在实现声明中。 (微软学习)

注意 / 限制

  • 一个 partial 构造函数或事件必须恰有一个定义声明(declaration)和一个实现声明(implementation)。
  • 使用时要确保不同片段一致的签名、访问修饰、属性等。
  • 在大型已有代码中引入可能增加维护复杂度。

8. 用户定义的复合赋值运算符(User-defined Compound Assignment Operators)

动机 / 意义
C# 支持用户定义运算符重载(如 +, -),但对于复合赋值(如 +=, -= 等)通常是基于基础运算符组合。但有时候你可能希望更精细控制复合赋值的行为,例如避免中间重复计算或优化性能。C# 14 允许你为某些类型定义自己的复合赋值运算符。

语法与用法

public struct MyNumber
{
    public int Value;

    public static MyNumber operator +(MyNumber a, MyNumber b) 
        => new MyNumber { Value = a.Value + b.Value };

    public static MyNumber operator +=(ref MyNumber a, MyNumber b)
    {
        a.Value += b.Value;
        return a;
    }
}

这样,当你使用 x += y; 时,将调用你自定义的 operator +=,而不是简单地展开为 x = x + y
(注意:这只是语义说明,具体语法和支持的重载签名需参考最终规范。)

注意 / 限制

  • 自定义复合赋值运算符的签名可能更严格,不能随意定义。
  • 在设计时要避免与基础运算符重载产生一致性问题(例如 x += yx = x + y 的行为应尽可能一致)。
  • 应谨慎引入,以避免读者困惑或滥用。

9. 单文件 C# 应用(File-based Apps / 脚本运行支持)

动机 / 意义
在早期版本中,使用 C# 总是需要项目文件(.csproj)和文件结构,这在写一点测试代码、脚本、临时代码时显得繁琐。C# 14 / .NET 10 引入文件级应用支持,可以直接运行单个 .cs 文件 — 类似脚本语言体验。(InfoWorld)

用法

dotnet run myscript.cs

你可以在 .cs 文件中使用源文件级指令声明(例如包引用、SDK 版本、属性等),而无需项目文件。执行时,.NET CLI 会隐性构建运行环境。(InfoWorld)

示例(myscript.cs):

// &lt;auto-generated>  // 或者 文件级指令
using System;

Console.WriteLine("Hello from single-file app!");

然后:

dotnet run myscript.cs

注意 / 限制

  • 虽然简化了快速测试场景,但对于复杂应用(多个类、多个文件、依赖库等)仍建议使用传统项目结构。
  • IntelliSense、调试、编辑器支持在早期阶段可能不那么完整。
  • 源文件级指令语法、包引用语法等可能会在演进中调整。

三、升级 & 使用建议

在你的项目中尝试或采用 C# 14 新特性时,建议注意以下几点:

建议说明
启用预览语言版本.csproj 中设置 <LangVersion>preview</LangVersion> 或明确指定 14
渐进引入优先在非核心模块、工具类、内部库中试用;待稳定后才在业务核心代码中使用。
兼顾阅读性虽然新语法简洁,但过度使用可能让新团队成员不易理解,建议在团队中达成一致风格。
注意重载 / 二义性风险如隐式转换、重载优先级变更等可能引入意外行为。升级时要做详尽测试。
关注编译器 / 语言变更日志特性预览阶段可能调整,定期查看 Roslyn / C# 语言团队发布的更新和 Breaking Changes 文档。
代码回退策略对于还在维护多个目标框架或旧版 C# 的项目,要保留兼容路径或条件编译。