扩展方法可以给已有的类型添加方法,而不需要修改它、继承它或包装它。你写一个普通的静态方法,用 this 标记它的第一个参数,调用方就能用点语法使用它,就好像这个类型本来就有这个方法一样:
输出:
Extension methods let yo...
12
short
Extension...
title.Truncate(24) 会被编译成完全等同的 StringExtensions.Truncate(title, 24)。点语法只是一种便利:方法仍然是静态的,仍然位于 StringExtensions 中,而且只能使用 string 的公共成员。
声明扩展方法的规则
- 方法必须是
static的,并且位于一个static、非泛型、没有嵌套在其他类中的类里。非静态或泛型的类会报 CS1106:Extension method must be defined in a non-generic static class;嵌套的类会报 CS1109:Extension methods must be defined in a top level static class。 this只能放在第一个参数上,这个参数的类型就是被扩展的类型。其余参数都是普通参数。- 方法本身可以是泛型的(
this IEnumerable<T> source),即使类不能是泛型的。 - 它能看到的只有任何外部代码都能看到的东西:公共成员(以及同一程序集内的 internal 成员)。私有字段仍然是私有的。
惯例是按被扩展的类型给类命名(StringExtensions、EnumerableExtensions),并把它们归到 MyApp.Extensions 这样的命名空间中。
扩展接口与 IEnumerable<T>
扩展一个接口,就等于给实现它的每一个类型都加上了这个方法。因此,扩展 IEnumerable<T> 会一次性把方法加给数组、列表、集合、字典的键集合以及 LINQ 查询结果:
输出:
9.5
50
65
f0 f3 f6
EveryNth 是泛型的:T 从接收者推断出来,所以它在这里用于字符串,也适用于任何其他元素类型。因为它使用了 yield return,所以它和内置的 LINQ 运算符一样是延迟执行的,并且可以与它们链式调用。
LINQ 建立在扩展方法之上
Where、Select、OrderBy、Sum、First 以及 LINQ 的其余部分,都是 IEnumerable<T> 上的扩展方法,定义在静态类 System.Linq.Enumerable 中。这就是为什么文件顶部必须有 using System.Linq;,list.Where(...) 才能编译通过;也是为什么 LINQ 能用于每一种集合类型,而它们没有一个实现了 Where 方法:
输出:
72, 88, 95
72, 88, 95
这两行是同样的代码。扩展语法把从里往外读的嵌套调用,变成了按步骤发生顺序阅读的链式调用,这正是扩展方法存在的主要原因:它们是在 C# 3 中与 LINQ 一起加入的。LINQ 那一页介绍了各个运算符。
必须导入命名空间
只有用 using 导入了扩展方法所在静态类的命名空间,它才在作用域内。否则调用会失败,就好像这个方法不存在一样:
error CS1061: 'string' does not contain a definition for 'Truncate' and no accessible extension method 'Truncate' accepting a first argument of type 'string' could be found (are you missing a using directive or an assembly reference?)
为扩展方法的命名空间加上 using(或者从 C# 10 起,为整个项目写一次 global using):
输出:
EUR 49.90
反过来看,这一点也很有用:不想到处可见的扩展方法,可以放在只有部分文件才导入的命名空间里。
实例方法优先
如果类型已经有一个适用的实例方法,编译器就会用它,根本不会考虑扩展方法。签名相同的扩展方法,在每个使用点语法的调用处都是无效代码:
输出:
Invoice (instance method): 120
Invoice (extension): 120 USD
extension: only reachable as a static call
只有一个参数的扩展方法输给了实例方法;有两个参数的那个被使用了,因为没有哪个实例方法接收字符串参数。这也意味着,如果库的某次更新添加了一个与你的扩展方法同名的实例方法,你的调用就会悄无声息地切换到库的版本。请选择不太可能冲突的扩展方法名。
在 null 上调用
由于这个调用实际上是静态调用,null 接收者不会在调用处抛出异常。方法收到 null 作为第一个实参,并决定这意味着什么:
输出:
True
AL
ArgumentNullException: fullName
missing.IsBlank() 返回 true 很方便,但可能会让以为在 null 上调用成员就会抛出异常的读者感到意外。只在名称一看就明白的方法中接受 null(IsBlank、OrEmpty);其他地方,像 LINQ 那样抛出 ArgumentNullException。
扩展枚举
枚举不能有自己的方法,这让它们成为扩展方法的天然目标:
enum OrderStatus { Pending, Paid, Shipped, Delivered, Cancelled }
static class OrderStatusExtensions
{
public static bool IsFinal(this OrderStatus s) =>
s == OrderStatus.Delivered || s == OrderStatus.Cancelled;
}
// usage: if (order.Status.IsFinal()) { ... }
C# 14 中的扩展成员
在 C# 14 之前,只有方法可以作为扩展。C# 14 新增了 extension 块,它把同一个接收者的成员归在一起,还允许扩展属性和静态成员:
// C# 14 and later
public static class StringExtensions
{
extension(string s)
{
public bool IsBlank => string.IsNullOrWhiteSpace(s); // extension property
public string Truncate(int max) => s.Length <= max ? s : s[..max] + "...";
}
}
经典的 this 参数扩展方法依然有效,几乎所有现有代码和库用的都是它。
什么时候该写扩展方法
当类型不归你所有(string、DateTime、框架接口)、当你希望某个接口的每个实现都有同一个辅助方法,或者当链式调用比嵌套调用更易读时,扩展方法很合适。如果类是你自己的,就直接添加真正的实例方法:它可以使用私有状态,而且出现在读者会去找的地方。另外,避免扩展 object,那会让你的方法出现在程序中每个值的自动补全列表里。
常见问题
C# 中的扩展方法是什么?
扩展方法是一种静态方法,调用时就像它是另一个类型的实例方法一样。你把它写在静态类中,并在第一个参数前加上 this:public static bool IsBlank(this string s) => string.IsNullOrWhiteSpace(s);。这样,任何字符串都能使用 name.IsBlank(),而无需修改或继承 string。
为什么找不到我的扩展方法?
只有导入了扩展方法所在的命名空间,它才可见。如果静态类在 MyApp.Extensions 中,就在调用它的文件里加上 using MyApp.Extensions;;否则编译器会报 CS1061,说这个类型不包含该名称的定义,也找不到可访问的同名扩展方法。另外还要检查这个类是否是 static 的、非泛型的、没有嵌套在其他类中。
扩展方法能覆盖实例方法吗?
不能。编译器会先查找适用的实例方法,只有在找不到时才考虑扩展方法。与实例方法同名且参数相同的扩展方法,在每个使用实例语法的调用处都会被悄悄忽略。
C# 中可以在 null 上调用扩展方法吗?
可以。这个调用会编译成一次以 null 为第一个实参的静态方法调用,所以调用本身不会抛出 NullReferenceException。由方法决定怎么处理:把 null 当作合法输入(类似 IsNullOrEmpty 那样的辅助方法),或者像 LINQ 的方法那样抛出 ArgumentNullException。
C# 中有扩展属性吗?
C# 14 之前没有。在那之前,只有方法可以作为扩展,所以本想写成属性的东西只能写成方法(GetFullName())。C# 14 新增了扩展块,除了方法,还可以声明扩展属性和静态成员。