使用扩展方法扩展 .NET 库






4.21/5 (20投票s)
通过扩展方法,您可以为 .NET 对象添加新功能。
引言
随着 .NET 3.0 中引入 LINQ,Microsoft 添加了直接扩展对象功能的能力,而无需进行子类化。本文将介绍编写扩展方法的基础知识。演示的技术需要 Visual Studio 2008 或更高版本,或者早期版本的 Visual Studio 以及 .NET 3.0 框架。
扩展方法
编写扩展方法非常容易。在 Visual Basic 中,您在一个模块中编写方法,添加来自 `System.Runtime.CompilerServices` 命名空间的 `<Extension()>
` 属性,并将第一个参数设置为要扩展的对象类型。在 C# 中,您创建一个 `static` 类,将方法添加为 `static` 成员,并将第一个参数设置为要扩展的对象类型,使用 `this` 修饰符。现在,当您在该类型对象的 IntelliSense 弹出时,您的扩展方法将显示带有“扩展”图标。

有一些限制需要注意。您只能编写扩展 **方法**,而不能编写属性或事件。扩展方法可以重载现有方法,但不能覆盖它们。如果您的扩展方法与调用代码不在同一命名空间中,则必须使用 `Imports` (VB) 或 `using` (C#) 语句将扩展引入作用域。
扩展字符串:基础知识
让我开始思考扩展的原因是 `String` 对象:特别是,缺乏不区分大小写的 `Replace` 方法。我正在为内部应用程序添加合并功能,并将有一个包含占位符的基本文档;这些占位符将被数据库中的数据替换。由于这些文档由用户编写,我对占位符的大小写控制很少。
我在 `Microsoft.VisualBasic` 命名空间中找到了解决方案的开端。该库有一个 `Strings` 工具箱,其中包含历史上在 VB 中可用的方法。`Strings.Replace` 方法允许编码员选择二进制(区分大小写)或文本(不区分大小写)搜索。
第一个问题是 `Strings.Replace` 是一个工具箱方法而不是内置方法,这使得使用起来可能很麻烦。它有很多参数,大多数情况下都不需要,因此它的语法与标准的 `Replace` 方法不同。最后,许多 C# 编码员不喜欢使用 Visual Basic 库。编写扩展方法可以隐藏这些问题。在 VB 中看起来是这样的:
Namespace MyExtensions
<HideModuleName()> _
Public Module ExtensionModule
<System.Runtime.CompilerServices.Extension()> _
Public Function MyReplace(ByVal Str As String, _
ByVal Find As String, _
ByVal Replacement As String) As String
Return Microsoft.VisualBasic.Strings.Replace(Str, _
Find, _
Replacement, _
Compare:=CompareMethod.Text)
End Function
End Module
End Namespace
在 C# 中是这样的:
namespace MyExtensions
{
public static class ExtensionMethods
{
public static String MyReplace(this String Str,
String Find,
String Replacement)
{
return Microsoft.VisualBasic.Strings.Replace(Str,
Find,
Replacement,
1,
-1,
CompareMethod.Text);
}
}
}
您会注意到第一个参数保存着对象本身的值,在本例中是 `String`,这允许您在方法中使用它。
如果您导入或使用了 `MyExtensions` 命名空间,IntelliSense 现在会在所有 `String` 对象上显示 `MyReplace` 方法。在 C# 中,`string` 类型会映射到 .NET 的 `String` 对象,因此您的扩展可以从任一类型访问。
Dim S As String = "ABCDEFGHI"
Dim S1 = S.MyReplace("hi", "yz")
此代码创建一个名为 `S` 的 `String` 对象并为其赋值。创建第二个 `String` 对象 `S1`,并使用扩展方法为其赋值。因为 `MyReplace` 执行不区分大小写的替换,所以 `S1` 将被赋值为 "ABCDEFGyz"。
您可能已经注意到上面的示例使用了值类型作为第一个参数。这并非必需,至少在 VB 中不是:通过使用引用参数,您可以创建修改调用对象本身的扩展方法。考虑此方法:
Namespace MyExtensions
<HideModuleName()> _
Public Module ExtensionModule
<System.Runtime.CompilerServices.Extension()> _
Public Sub MyReplaceRef(ByRef Str As String, _
ByVal Find As String, _
ByVal Replacement As String)
Str = Microsoft.VisualBasic.Strings.Replace(Str, _
Find, _
Replacement, _
Compare:=CompareMethod.Text)
End Sub
End Module
End Namespace
由于 `Str` 参数是按引用传递的,因此您可以重新分配其值。使用此方法,您可以执行以下操作:
Dim S As String = "ABCDEFGHI"
S.MyReplaceRef("hi", "yz")
运行此代码后,`S` 的值为 "ABCDEFGyz"。
我未能用 C# 编写类似的功能;当我在第一个参数中传递指针时,编译器会报错。再说,我以 VB 为生,所以可能遗漏了一些显而易见的东西。如果有人知道如何做到这一点,请告诉我。
扩展字符串:重载
上面的示例使用了不同的扩展方法名称。但是,如果我想将其命名为 `Replace` 呢?
您可以编写重载现有方法的扩展,前提是您的扩展具有不同的 *有效* 签名;不要计算提供被扩展对象的第一个参数。如果您只是将 `MyReplace` 更改为 `Replace`,您的扩展将不会出现,因为它的有效签名是 `String String`,与两个内置的 `String.Replace` 方法之一相同。要使其可用,您必须更改签名。通过添加一个布尔参数来指示您的替换是否区分大小写,我们可以轻松做到这一点。修改后的方法看起来是这样的:
Namespace MyExtensions
<HideModuleName()> _
Public Module ExtensionModule
<System.Runtime.CompilerServices.Extension()> _
Public Function Replace(ByVal Str As String, _
ByVal Find As String, _
ByVal Replacement As String, _
ByVal CaseSensitive as Boolean) As String
If CaseSensitive Then
Return Microsoft.VisualBasic.Strings.Replace(Str, _
Find, _
Replacement, _
Compare:=CompareMethod.Binary)
Else
Return Microsoft.VisualBasic.Strings.Replace(Str, _
Find, _
Replacement, _
Compare:=CompareMethod.Text)
End If
End Function
End Module
End Namespace
在 C# 中是这样的:
namespace MyExtensions
{
public static class ExtensionMethods
{
public static String Replace(this String Str,
String Find,
String Replacement,
bool CaseSensitive)
{
if (CaseSensitive)
return Microsoft.VisualBasic.Strings.Replace(Str,
Find,
Replacement,
1,
-1,
CompareMethod.Binary);
else
return Microsoft.VisualBasic.Strings.Replace(Str,
Find,
Replacement,
1,
-1,
CompareMethod.Text);
}
}
}
现在,IntelliSense 将显示有三种形式的 `String.Replace`;第三种将具有三个参数签名,并被标记为扩展。

在 Visual Basic 中,可以编写带有可选参数的扩展方法(C# 不支持可选参数)。
Namespace MyExtensions
<HideModuleName()> _
Public Module ExtensionModule
<System.Runtime.CompilerServices.Extension()> _
Public Function Replace(ByVal Str As String, _
ByVal Find As String, _
ByVal Replacement As String, _
Optional ByVal CaseSensitive as Boolean = False) As String
If CaseSensitive Then
Return Microsoft.VisualBasic.Strings.Replace(Str, _
Find, _
Replacement, _
Compare:=CompareMethod.Binary)
Else
Return Microsoft.VisualBasic.Strings.Replace(Str, _
Find, _
Replacement, _
Compare:=CompareMethod.Text)
End If
End Function
End Module
End Namespace
这将正常编译并通过 IntelliSense 显示。但是,如果您尝试使用它,您会遇到麻烦。
Dim S As String = "ABCDEFGHI"
Dim S1 = S.Replace("hi", "yz")
问题在于编译器会 *首先* 检查内置方法。仅当没有内置方法匹配时,它才会检查扩展。
在上面的示例中,`Replace` 是用 `String String` 签名调用的。因为这与其中一个内置 `String.Replace` 方法的签名匹配,所以编译器将使用内置方法而不是您的代码。经验是,如果您要编写带有可选参数的扩展方法,请确保必需参数使您的方法签名独特。
超越字符串
当然,上述技术不仅限于 `String` 对象。例如,您可以编写一个 `DateTime` 扩展,它返回一个常用的文本格式:
<System.Runtime.CompilerServices.Extension()> _
Public Function ToStringAnsi(ByVal DT As DateTime) As String
Return DT.ToString("yyyy-MM-dd")
End Function
public static String ToStringAnsi(this DateTime DT)
{
return DT.ToString("yyyy-MM-dd");
}
或者您可能经常需要一个十进制变量的倒数:
<System.Runtime.CompilerServices.Extension()> _
Public Function Invert(ByVal D As Decimal) As Decimal
Return 1 / D
End Function
public static Decimal Invert(this Decimal D)
{
return (1 / D);
}
扩展泛型类型
任何对象都可以扩展,甚至是泛型对象。假设您想在泛型 `List` 上创建一个 `Maximum` 方法,该方法返回列表中值最大的项。您通过将扩展本身声明为泛型来开始,这会将您的强类型引入进来。您也可以像任何其他泛型声明一样应用约束。然后,使用该类型设置您的其他参数并进行编码。
<System.Runtime.CompilerServices.Extension()> _
Public Function Maximum(Of T As IComparable)(ByVal L As List(Of T), _
ByVal Comparer As IComparer(Of T)) As T
If L Is Nothing Then Return Nothing
Dim Max As T = L(0)
For Each Item As T In L
If Comparer.Compare(Item, Max) > 0 Then Max = Item
Next
Return Max
End Function
public static T Maximum<T>(this List<T> L, IComparer<T> Comparer)
where T: IComparable
{
if (L == null) return default(T);
T Max = L[0];
foreach (T Item in L)
{
if (Comparer.Compare(Item, Max) > 0) Max = Item;
}
return Max;
}
在这些代码示例中,请注意,`T` 被要求实现 `IComparable`;这将自动化许多安全检查。因为扩展被声明为泛型,所以您可以将 `T` 传递给您方法的其余部分,包括 `L` 和 `Comparer` 的声明。代码会验证确实有一个有效的 `List`,记录列表中的第一项,然后对列表中的每一项调用 `Comparer.Compare`,如果在找到更大的值时更新临时存储。遍历完列表后,临时存储将保存最大值并返回。
Public Sub Test()
Dim L As New List(Of String)
L.Add("Alpha")
L.Add("Beta")
L.Add("Gamma")
L.Add("Delta")
System.Diagnostics.Debug.WriteLine(L.Maximum(StringComparer.CurrentCulture))
End Sub
public void Test()
{
List<String> L = new List<String>();
L.Add("Alpha");
L.Add("Beta");
L.Add("Gamma");
L.Add("Delta");
System.Diagnostics.Debug.WriteLine(L.Maximum(StringComparer.CurrentCulture));
}
`StringComparer` 为 `String` 对象提供了多种比较方法;`CurrentCulture` 根据工作站的区域设置执行区分大小写的字符串比较。假设基于英语的设置,这些示例将返回 "Gamma"。
VB 和 C# 版本之间存在一个应注意的奇怪差异。VB 方法可以返回 `Nothing`;C# 中的泛型显然不能返回 `null`,因此如果 `L == null`,我让方法返回 `T` 的默认值。
您还可以扩展具有两个或更多泛型参数的对象,例如 `Dictionary(Of T1, T2)`。
扩展接口
严格来说,您扩展的对象不必是对象:您也可以扩展接口。假设您想要所有实现 `ICollection` 而不仅仅是 `List` 对象的 `Maximum` 方法。您可以通过将 `List` 替换为 `ICollection` 来非常轻松地做到这一点,如下所示:
<System.Runtime.CompilerServices.Extension()> _
Public Function Maximum(Of T As IComparable)(ByVal IC as ICollection(Of T), _
ByVal Comparer As IComparer(Of T)) As T
If IC Is Nothing Then Return Nothing
Dim Max As T = IC(0)
For Each Item As T In IC
If Comparer.Compare(Item, Max) > 0 Then Max = Item
Next
Return Max
End Function
public static T Maximum<T>(this ICollection<T> IC, IComparer<T> Comparer)
where T: IComparable
{
if (L == null) return default(T);
T Max = default(T);
foreach (T Item in IC)
{
if (Comparer.Compare(Item, Max) > 0) Max = Item;
}
return Max;
}
现在,`Maximum` 可用于列表、字典、数组以及实现 `ICollection` 的任何其他对象,即使这些对象是内在创建的。

这里 C# 再次施加了一个 VB 中不存在的约束。在 Visual Basic 中,我可以将 `Max` 设置为集合中的第一项;C# 不允许这样做,因此 `Max` 使用 `default(T)` 初始化。
当然,接口不必是泛型的。
最佳实践
扩展方法是非常强大的工具,巨大的力量伴随着巨大的责任。2007 年,Microsoft 的 Visual Basic 团队发布了一篇题为 **扩展方法最佳实践** 的博客文章,可在此 处 找到。如果您打算大量使用此技术,最好回顾一下这些建议。
历史
- 2009 年 3 月 20 日:初始发布
- 2009 年 3 月 31 日:清理和澄清了文本并修复了一些格式问题。
- 2009 年 4 月 1 日:进行了一些小的更正。