.NET 10 LTS(2025.11)· C# 3 以来对扩展机制的最大升级 · 属性、静态扩展、运算符
2007 年 C# 3 引入扩展方法。语法一直没变:static 方法 + this 修饰第一个参数。它能做的事仅限于方法调用:
// C# 3~13 的扩展方法——17 年不变的语法
public static class EnumerableExtensions
{
public static bool IsEmpty<T>(this IEnumerable<T> source)
=> !source.Any();
}
// 使用:方法调用——还行
if (items.IsEmpty()) { ... }
// 但你不能写:
// if (items.IsEmpty) { ... } ← 属性!不支持
// var combined = list1 + list2; ← 运算符!不支持
// var identity = IEnumerable<int>.Empty; ← 静态扩展!不支持
items.IsEmpty)、不能加静态成员、不能加运算符。LINQ 的设计者想要这些——但没有语法支持。
// C# 14:一个 extension 块 = 一组扩展成员
public static class MyExtensions
{
extension<T>(IEnumerable<T> source) // ← 实例扩展块
{
public bool IsEmpty => !source.Any(); // 扩展属性!
public T FirstOr(T fallback) // 扩展方法(无 this)
=> source.FirstOrDefault() ?? fallback;
}
extension<T>(IEnumerable<T>) // ← 静态扩展块(无参数名)
{
public static IEnumerable<T> Empty // 静态扩展属性!
=> Enumerable.Empty<T>();
public static IEnumerable<T> operator + // 扩展运算符!
(IEnumerable<T> left, IEnumerable<T> right)
=> left.Concat(right);
}
}
三个新能力一次解锁:扩展属性、静态扩展、扩展运算符。
// 每个扩展独立声明
public static bool IsEmpty<T>(
this IEnumerable<T> source) { }
public static T FirstOr<T>(
this IEnumerable<T> source,
T fallback) { }
// 无法表达:属性、静态、运算符
extension<T>(IEnumerable<T> source)
{
public bool IsEmpty => ... // 属性
public T FirstOr(T fb) => ... // 方法
}
extension<T>(IEnumerable<T>)
{
public static ... Empty => ... // 静态属性
public static ... operator + // 运算符
}
| 要素 | 必填? | 说明 |
|---|---|---|
类型参数 <T> | 可选 | 可带约束 where T : IEquatable<T> |
| 接收者类型 | 必填 | 被扩展的类型,如 IEnumerable<T> |
| 参数名 | 可选 | 有名字 → 实例扩展块(可声明属性和实例方法)。无名 → 静态扩展块(只能声明 static 成员) |
| 外层类 | — | 必须是非泛型、非嵌套的 static class |
public static class StringExtensions
{
// ── 实例扩展块:有参数名 "text" ──
extension(string text) // text 在块内可引用
{
public bool IsNullOrEmpty // 实例属性
=> string.IsNullOrEmpty(text); // ← text 被捕获引用
public string Truncate(int max) // 实例方法
=> text.Length <= max ? text : text[..max];
}
// ── 静态扩展块:无参数名 ──
extension(string) // 只有类型,没有名字
{
public static string Default => ""; // 静态属性
public static string operator *(string s, int n) // 静态运算符
=> string.Concat(Enumerable.Repeat(s, n));
}
}
// ── 调用 ──
var s = "Hello World";
Console.WriteLine(s.IsNullOrEmpty); // False — 实例扩展属性
Console.WriteLine(s.Truncate(5)); // "Hello" — 实例扩展方法
Console.WriteLine(string.Default); // "" — 静态扩展属性
Console.WriteLine("ab" * 3); // "ababab" — 扩展运算符
extension 块被编译成多个合成类型。以这个简单的扩展属性为例:
// ──── 你写的代码 ────
public static class E
{
extension<T>(IEnumerable<T> source)
{
public bool IsEmpty => !source.Any();
}
}
// ──── 编译器生成的等效结构(简化) ────
public static class E
{
// ① 分组类型——按 CLR 签名分组
[CompilerGenerated]
sealed class <>GroupingType_1<T>
{
// ② 标记类型——保留完整的 C# 级签名
[CompilerGenerated]
static class <>MarkerType
{
[CompilerGenerated]
static void <Extension>$(IEnumerable<T> source) { }
}
// ③ 骨架成员——保留原始签名,体为 throw null
[ExtensionMarkerName("<>MarkerType")]
public bool IsEmpty => throw null;
}
// ④ 实际实现方法——在顶层 static class 中
[Extension]
public static bool IsEmpty<T>(IEnumerable<T> source)
=> !source.Any();
}
// ──── 调用 items.IsEmpty 被编译为 ────
// E.IsEmpty(items) ← 普通的静态方法调用
[Extension] 特性的普通静态方法。合成类型(分组类型、标记类型)只存在于元数据中,用于 IDE 导航和重载决议。
经典扩展方法的一个问题是——重载决议只看 CLR 签名。C# 的 null 可空注解、ref 修饰符等在 CLR 层面被擦除。标记类型保留了完整的 C# 级签名(含参数名、refness、nullable),使编译器能在调用点精确匹配。
// 两个扩展块——CLR 层面可能"看起来一样"
extension(string? text) { /* nullable string */ }
extension(string text) { /* non-null string */ }
// ↑ 标记类型保留了 C# 级的 ? 差异,编译器能正确分辨
最自然的应用——给集合类型加属性和运算符:
public static class CollectionExtensions
{
extension<T>(IEnumerable<T> source)
{
public bool IsEmpty => !source.Any();
public bool IsNotEmpty => source.Any();
public T? FirstOrNull => source.FirstOrDefault(); // 属性!
public int Count => source.Count(); // 属性!
}
extension<T>(IEnumerable<T>)
{
public static IEnumerable<T> operator +
(IEnumerable<T> left, IEnumerable<T> right)
=> left.Concat(right);
}
}
// 使用——像操作一等公民一样操作集合:
if (orders.IsNotEmpty) // 属性读起来比 IsNotEmpty() 自然
{
var all = activeOrders + archivedOrders; // 运算符!
Console.WriteLine($"共 {all.Count} 条"); // 属性!
}
items.IsEmpty(属性)比 items.IsEmpty()(方法)更自然——语义上"是否为空"是一个状态查询,不是动作。属性正是为状态查询设计的。
// Domain 层——纯净的 DTO
public class OrderDto
{
public int Id { get; set; }
public decimal Amount { get; set; }
public DateTime CreatedAt { get; set; }
}
// UI 层——显示逻辑(扩展属性)
public static class OrderPresentationExtensions
{
extension(OrderDto order)
{
public string DisplayAmount => $"¥{order.Amount:N2}";
public string TimeAgo => (DateTime.Now - order.CreatedAt) switch
{
{ TotalHours: < 1 } => "刚刚",
{ TotalHours: < 24 } => $"{(int)(DateTime.Now - order.CreatedAt).TotalHours}小时前",
var d => $"{d.Days}天前"
};
}
}
// 使用:DTO 保持纯净,展示逻辑在扩展中
<div>@order.DisplayAmount · @order.TimeAgo</div>
public struct Counter { public int Value; }
public static class CounterExtensions
{
extension(ref Counter c) // ← ref 修饰——按引用传递
{
public void Increment() => c.Value++;
public void Reset() => c.Value = 0;
}
}
var c = new Counter { Value = 1 };
c.Increment();
Console.WriteLine(c.Value); // 2——直接修改了原 struct
c.Reset();
Console.WriteLine(c.Value); // 0
private 成员仍然不可访问。
这是最重要的规则:类型自身的成员永远优先于扩展成员。扩展成员只是"候补"——只有类型自身没有匹配成员时才会被选中。
public class Person
{
public string FullName => $"{First} {Last}"; // ← 自身有 FullName
public string First { get; set; }
public string Last { get; set; }
}
public static class Ext
{
extension(Person p)
{
public string FullName => "from extension"; // 永远不会被调用!
}
}
var p = new Person { First = "Alice", Last = "Smith" };
Console.WriteLine(p.FullName); // "Alice Smith"——类自身的 FullName 胜出
IsReady 属性。你的代码 obj.IsReady 编译通过。然后你升级到 v2——包的作者决定把 IsReady 提升为类型自身的实例属性。如果同名报错:你的项目炸了。一行代码没改,升级依赖就编译不过。
using 引入的是"可选能力",不是"潜在地雷"。如果导入某个命名空间会导致编译错误,开发者会犹豫要不要写 using——这违背了扩展机制的初衷。17 年来 LINQ 能安全运行,正是依赖这个语义:哪天 List<T> 加了 Where() 实例方法,所有 LINQ 代码也不能挂。
| 类别 | 限制 |
|---|---|
| 禁止的修饰符 | abstract、virtual、override、new、sealed、partial、protected 系列 |
| 属性限制 | 不能有 init 访问器(只能 get/set) |
| 外层类 | 必须是 non-generic、non-nested、static class |
| 作用域 | 通过 using 命名空间引入——与经典扩展方法相同 |
| 命名冲突 | 成员名不能与外层静态类名或扩展类型名相同 |
| 保留字 | extension 成为新关键字——类型和别名不能再叫 extension |
| 入口点 | extension 块中的方法不能作为程序入口点(Main) |
| 可见性 | 遵守被扩展类型的成员可见性——不能访问 private 成员 |
// Extensions.cs —— 在 MyApp.Extensions 命名空间中
namespace MyApp.Extensions;
public static class StringExt
{
extension(string s)
{
public bool IsUrl => Uri.TryCreate(s, UriKind.Absolute, out _);
}
}
// ──── 另一个文件 ────
using MyApp.Extensions; // ← 必须显式导入!否则 IsUrl 不可见
var url = "https://example.com";
Console.WriteLine(url.IsUrl); // True——using 引入后才可见
| 场景 | C# 3~13 经典写法 | C# 14 extension 块 |
|---|---|---|
| 实例方法 | public static bool Foo(this T x, int y) |
extension(T x) { public bool Foo(int y) => ...; } |
| 实例属性 | ❌ 不支持 | extension(T x) { public bool P => ...; } |
| 静态方法 | ❌ 不支持(只能用普通 static 方法) | extension(T) { public static T Foo() => ...; } |
| 静态属性 | ❌ 不支持 | extension(T) { public static T Default => ...; } |
| 运算符 | ❌ 不支持 | extension(T) { public static T operator +(T a, T b) => ...; } |
| ref 扩展 | public static void Foo(this ref T x) |
extension(ref T x) { public void Foo() => ...; } |
| 编译产物 | static 方法 + [Extension] | 同左——完全兼容 |
以下哪个 extension 块声明有语法错误?
以下代码输出什么?
public class Box
{
public string Label => "instance";
}
public static class Ext
{
extension(Box b)
{
public string Label => "extension";
public string Tag => "extension-tag";
}
}
var box = new Box();
Console.WriteLine($"{box.Label}/{box.Tag}");
以下代码输出什么?
public struct Num { public int V; }
public static class Ext
{
extension(Num n) { public void ByVal() => n.V = 99; }
extension(ref Num n) { public void ByRef() => n.V = 99; }
}
var x = new Num { V = 1 };
x.ByVal();
Console.Write(x.V + " ");
x.ByRef();
Console.Write(x.V);
以下关于 extension 块编译产物的描述,哪个是正确的?
以下哪个场景最适合用 extension 块(而非其他方案)?
extension 块是 C# 14 的旗舰特性——它从根本上改变了扩展的叙事。但 C# 14 还有很多好东西:Lambda 参数修饰符的无类型简化、Null 条件赋值、nameof 支持未绑定泛型。
📖 NRT 速查 · ← L21: field 关键字 · L23: Lambda 修饰符 + Null 赋值 + nameof 泛型 →