65.9K
CodeProject 正在变化。 阅读更多。
Home

使用扩展方法扩展 .NET 库

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.21/5 (20投票s)

2009年3月20日

CPOL

7分钟阅读

viewsIcon

68908

通过扩展方法,您可以为 .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 弹出时,您的扩展方法将显示带有“扩展”图标。

ExtensionType.PNG

有一些限制需要注意。您只能编写扩展 **方法**,而不能编写属性或事件。扩展方法可以重载现有方法,但不能覆盖它们。如果您的扩展方法与调用代码不在同一命名空间中,则必须使用 `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`;第三种将具有三个参数签名,并被标记为扩展。

Overloaded.PNG

在 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` 的任何其他对象,即使这些对象是内在创建的。

Interface.PNG

这里 C# 再次施加了一个 VB 中不存在的约束。在 Visual Basic 中,我可以将 `Max` 设置为集合中的第一项;C# 不允许这样做,因此 `Max` 使用 `default(T)` 初始化。

当然,接口不必是泛型的。

最佳实践

扩展方法是非常强大的工具,巨大的力量伴随着巨大的责任。2007 年,Microsoft 的 Visual Basic 团队发布了一篇题为 **扩展方法最佳实践** 的博客文章,可在此 找到。如果您打算大量使用此技术,最好回顾一下这些建议。

历史

  • 2009 年 3 月 20 日:初始发布
  • 2009 年 3 月 31 日:清理和澄清了文本并修复了一些格式问题。
  • 2009 年 4 月 1 日:进行了一些小的更正。
© . All rights reserved.