下面是我整理的一份 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<T>(IEnumerable<T> source) // 为 IEnumerable<T> 定义扩展成员
{
// 扩展属性(read-only)
public bool IsEmpty => !source.Any();
// 扩展方法
public IEnumerable<T> WhereGreaterThan(T threshold)
=> source.Where(x => Comparer<T>.Default.Compare(x, threshold) > 0);
}
extension<T>(IEnumerable<T>) // 定义静态扩展成员(不针对实例)
{
public static IEnumerable<T> Combine(IEnumerable<T> a, IEnumerable<T> b)
=> a.Concat(b);
public static IEnumerable<T> Identity => Enumerable.Empty<T>();
public static IEnumerable<T> operator +(IEnumerable<T> a, IEnumerable<T> b)
=> a.Concat(b);
}
}
- 第一个
extension<T>(IEnumerable<T> source)
块定义“实例级扩展成员” —— 调用时像普通成员一样:myList.IsEmpty
。 - 第二个
extension<T>(IEnumerable<T>)
块定义“静态扩展成员” —— 调用时像类型成员一样:IEnumerable<int>.Identity
或IEnumerable<int>.Combine(a, b)
。 - 在这个语法中,也可以定义运算符重载扩展成员。 (Microsoft for Developers)
- 已有的 this-parameter 扩展方法仍然可以继续使用,两种语法共存。 (Microsoft for Developers)
注意 / 限制:
- 扩展成员仍是静态方法在底层被调用(编译器会“lower”到普通静态方法)。 (Microsoft for Developers)
- 并不是所有 this-parameter 扩展方法都能直接迁移为扩展成员语法(某些复杂泛型签名会受限)。 (Microsoft for Developers)
- 如果在类型中已有名为
field
、extension
或其他关键字的成员,需要用@
前缀绕开命名冲突。 - 此特性目前仍为预览状态。
示例使用:
var list = new List<int> { 1, 2, 3 };
if (list.IsEmpty)
{
Console.WriteLine("Empty");
}
var combined = IEnumerable<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 && 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<>)); // 输出 "List"
Console.WriteLine(nameof(Dictionary<,>)); // 输出 "Dictionary"
示例:
string s1 = nameof(List<>); // "List"
string s2 = nameof(Dictionary<,>); // "Dictionary"
相比以前只能写:
string s = nameof(List<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<int> span = new int[] { 1, 2, 3 }; // 隐式从数组转换
ReadOnlySpan<int> roSpan = span; // 隐式转换
int[] arr = span; // 隐式转换回数组(若安全)
span.SomeExtensionMethod(); // span 作为接收器使用扩展方法
注意 / 风险:
- 新的隐式转换可能会引发重载解析上的歧义。代码升级时要注意可能的重载优先级变化。
- 即便有这些隐式支持,仍要留意性能、边界检查等问题。
5. 简化 Lambda 参数上的修饰符(Modifiers on Simple Lambda Parameters)
动机 / 意义:
在以前版本中,如果 Lambda 参数带有 ref
、in
、out
、ref readonly
等修饰符,就必须为参数写出类型。不能用简化形态。C# 14 放宽这个限制,让这些修饰符可以直接写在简化 Lambda 语法上,从而减少冗余。
语法与用法:
例如:
delegate bool TryParse<T>(string text, out T result);
// 以前必须写完整参数类型
TryParse<int> p1 = (string text, out int result) => Int32.TryParse(text, out result);
// C# 14 允许省略类型,同时加修饰符
TryParse<int> p2 = (text, out result) => Int32.TryParse(text, out result);
// 甚至可以有 in/ref 等修饰符
Func<ReadOnlySpan<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 < 0) throw new ArgumentOutOfRangeException(nameof(value));
field = value;
}
}
注意 / 限制:
- 如果你的类中已有成员名叫
field
,可能会冲突,此时可以用@field
或this.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 += y
与x = 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):
// <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# 的项目,要保留兼容路径或条件编译。 |
发表回复