Lesson 22: C# 14 — 扩展成员 Extension Members

.NET 10 LTS(2025.11)· C# 3 以来对扩展机制的最大升级 · 属性、静态扩展、运算符

前置:已完成 Lesson 21(field 关键字)。本课是 C# 14 最大的新特性——从只能扩展方法,到可以扩展属性、静态成员、运算符。
阅读:Microsoft Learn: C# 14 新增功能 · 扩展成员编程指南 · C# 14 扩展成员设计提案

一、C# 3 的遗产:你只能扩展方法 The Limitation

1.1 17 年了——只有一个语法

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 的设计者想要这些——但没有语法支持。

1.2 C# 14 的答案:extension 块

// 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);
    }
}

三个新能力一次解锁:扩展属性静态扩展扩展运算符

😣 C# 3~13 — 只能加方法

// 每个扩展独立声明
public static bool IsEmpty<T>(
    this IEnumerable<T> source) { }

public static T FirstOr<T>(
    this IEnumerable<T> source,
    T fallback) { }

// 无法表达:属性、静态、运算符

😎 C# 14 — 扩展块

extension<T>(IEnumerable<T> source)
{
    public bool IsEmpty => ...   // 属性
    public T FirstOr(T fb) => ... // 方法
}
extension<T>(IEnumerable<T>)
{
    public static ... Empty => ... // 静态属性
    public static ... operator + // 运算符
}

二、语法解剖 Syntax Anatomy

2.1 extension 块的结构

extension<类型参数>(接收者类型 参数名) 约束 { 扩展方法 public T Foo(int x) => ...; 扩展属性 public bool IsReady => ...; 扩展运算符 public static T operator +(T a, T b) => ...; }
要素必填?说明
类型参数 <T>可选可带约束 where T : IEquatable<T>
接收者类型必填被扩展的类型,如 IEnumerable<T>
参数名可选有名字 → 实例扩展块(可声明属性和实例方法)。无名 → 静态扩展块(只能声明 static 成员)
外层类必须是非泛型、非嵌套的 static class

2.2 两种扩展块:实例 vs 静态

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" — 扩展运算符
关键区别:有参数名 → 实例成员(可声明属性和方法);无参数名 → 静态成员(可声明 static 方法、static 属性、运算符)。实例块不能声明 static 成员——反之亦然。

三、编译器生成了什么?Lowering Deep Dive

3.1 降低概览

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 块不引入任何新的运行时概念。最终产物是带有 [Extension] 特性的普通静态方法。合成类型(分组类型、标记类型)只存在于元数据中,用于 IDE 导航和重载决议。

3.2 为什么需要标记类型?

经典扩展方法的一个问题是——重载决议只看 CLR 签名。C# 的 null 可空注解、ref 修饰符等在 CLR 层面被擦除。标记类型保留了完整的 C# 级签名(含参数名、refness、nullable),使编译器能在调用点精确匹配。

// 两个扩展块——CLR 层面可能"看起来一样"
extension(string? text)    { /* nullable string */ }
extension(string text)     { /* non-null string  */ }

// ↑ 标记类型保留了 C# 级的 ? 差异,编译器能正确分辨

四、实战场景 Real-World Scenarios

4.1 LINQ 风格的流畅 API 日常

最自然的应用——给集合类型加属性和运算符:

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()(方法)更自然——语义上"是否为空"是一个状态查询,不是动作。属性正是为状态查询设计的。

4.2 分层架构——按层扩展 DTO 日常

// 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>

4.3 ref 扩展——直接修改 struct 进阶

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
注意:ref 扩展和 by-value 扩展必须用不同的 extension 块——ref 块和普通块不能混合。private 成员仍然不可访问。

五、规则与限制 Rules & Constraints

5.1 解析优先级——实例成员始终优先

这是最重要的规则:类型自身的成员永远优先于扩展成员。扩展成员只是"候补"——只有类型自身没有匹配成员时才会被选中。

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 胜出
🤔 为什么不直接报错?这可能是你读完上面代码的第一反应——既然实例成员始终优先,同名还有什么意义?为什么不给一个编译错误(或至少警告)?

答案是向前兼容。假设同名报错:你引用了第三方 NuGet 包 v1,它的扩展成员提供了 IsReady 属性。你的代码 obj.IsReady 编译通过。然后你升级到 v2——包的作者决定把 IsReady 提升为类型自身的实例属性。如果同名报错:你的项目炸了。一行代码没改,升级依赖就编译不过。

这是 C# 3 就定下的设计:扩展成员是 "候补队员"——主力上场了,候补就坐下,不需要裁判叫停比赛。using 引入的是"可选能力",不是"潜在地雷"。如果导入某个命名空间会导致编译错误,开发者会犹豫要不要写 using——这违背了扩展机制的初衷。17 年来 LINQ 能安全运行,正是依赖这个语义:哪天 List<T> 加了 Where() 实例方法,所有 LINQ 代码也不能挂。

至于警告——理论上可以,但 C# 团队选择不发。LINQ 场景会产生海量噪音(每个集合类型都可能"隐藏"若干 LINQ 扩展成员),且"实例优先"是确定性行为,不需要提醒。

5.2 完整限制清单

类别限制
禁止的修饰符abstractvirtualoverridenewsealedpartialprotected 系列
属性限制不能有 init 访问器(只能 get/set)
外层类必须是 non-generic、non-nested、static class
作用域通过 using 命名空间引入——与经典扩展方法相同
命名冲突成员名不能与外层静态类名或扩展类型名相同
保留字extension 成为新关键字——类型和别名不能再叫 extension
入口点extension 块中的方法不能作为程序入口点(Main)
可见性遵守被扩展类型的成员可见性——不能访问 private 成员

5.3 作用域——using 决定一切

// 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 引入后才可见

六、经典扩展方法 vs extension 块 Old vs New

场景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] 同左——完全兼容
兼容性:两种语法可以共存于同一个 static class 中。经典语法的实例扩展方法和 extension 块的实例扩展成员在同一个声明空间内——不能有签名冲突。对调用方而言,完全透明——两种语法产出的 IL 相同。

七、测验 Quiz

1/5 · 语法基础

以下哪个 extension 块声明有语法错误

2/5 · 解析优先级

以下代码输出什么?

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}");

3/5 · ref 扩展

以下代码输出什么?

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

4/5 · 编译器产物

以下关于 extension 块编译产物的描述,哪个是正确的?

5/5 · 实战判断

以下哪个场景最适合用 extension 块(而非其他方案)?

八、extension 块之后是什么?What's Next

extension 块是 C# 14 的旗舰特性——它从根本上改变了扩展的叙事。但 C# 14 还有很多好东西:Lambda 参数修饰符的无类型简化、Null 条件赋值、nameof 支持未绑定泛型。

C# 3
.NET 3.5
扩展方法
C# 13
.NET 9
field 预览
C# 14
.NET 10 LTS
扩展成员 ✅
→ Next
C# 14
修饰符+赋值+泛型

📖 NRT 速查 · ← L21: field 关键字 · L23: Lambda 修饰符 + Null 赋值 + nameof 泛型 →