扩展方法 (Visual Basic)

开发人员可通过扩展方法将自定义功能添加到已定义的数据类型,而无需创建新的派生类型。 通过扩展方法可编写一个可调用的方法,就像它是现有类型的实例方法一样。

注解

扩展方法只能是 Sub 过程或 Function 过程。 你不能定义扩展属性、字段或事件。 所有扩展方法都必须使用 System.Runtime.CompilerServices 命名空间中的扩展属性 <Extension> 进行标记,并且必须在模块中定义。 如果扩展方法在模块外定义,Visual Basic 编译器会生成错误 BC36551“只能在模块中定义扩展方法”。

扩展方法定义中的第一个参数指定方法要扩展哪个数据类型。 运行方法时,第一个参数绑定到调用该方法的数据类型的实例。

Extension 属性只能应用于 Visual Basic ModuleSubFunction。 如果将该属性应用于 ClassStructure,Visual Basic 编译器会生成错误 BC36550“Extension 属性只能应用于 Module、Sub 或 Function 声明”。

示例

下面的示例定义了对 String 数据类型的 Print 扩展。 该方法使用 Console.WriteLine 来显示字符串。 Print 方法的参数 aString 确定该方法扩展 String 类。

Imports System.Runtime.CompilerServices

Module StringExtensions

    <Extension()> 
    Public Sub Print(ByVal aString As String)
        Console.WriteLine(aString)
    End Sub

End Module

请注意,扩展方法定义使用扩展属性 <Extension()> 进行标记。 可根据需要标记定义了方法的模块,但每个扩展方法都必须进行标记。 必须导入 System.Runtime.CompilerServices 才能访问扩展属性。

只能在模块中声明扩展方法。 通常,定义扩展方法的模块与调用扩展方法的模块不同。 而是将导入包含扩展方法的模块(如果需要),以将其纳入范围。 包含 Print 的模块在范围中后,可以像调用不带参数的普通实例方法一样调用该方法,例如 ToUpper

Module Class1

    Sub Main()

        Dim example As String = "Hello"
        ' Call to extension method Print.
        example.Print()

        ' Call to instance method ToUpper.
        example.ToUpper()
        example.ToUpper.Print()

    End Sub

End Module

下一个示例 PrintAndPunctuate 也是 String 的扩展,这次使用两个参数进行定义。 第一个参数 aString 确定扩展方法扩展 String。 第二个参数 punc 应是一个标点符号字符串,在调用方法时作为参数传入。 该方法显示的字符串后跟标点符号。

<Extension()> 
Public Sub PrintAndPunctuate(ByVal aString As String, 
                             ByVal punc As String)
    Console.WriteLine(aString & punc)
End Sub

通过传入 punc 的字符串参数来调用该方法:example.PrintAndPunctuate(".")

下面的示例演示了对 PrintPrintAndPunctuate 的定义与调用。 在定义模块中导入 System.Runtime.CompilerServices 才能实现对扩展属性的访问。

Imports System.Runtime.CompilerServices

Module StringExtensions

    <Extension()>
    Public Sub Print(aString As String)
        Console.WriteLine(aString)
    End Sub

    <Extension()>
    Public Sub PrintAndPunctuate(aString As String, punc As String)
        Console.WriteLine(aString & punc)
    End Sub
End Module

接下来,将扩展方法纳入范围中并调用:

Imports ConsoleApplication2.StringExtensions

Module Module1

    Sub Main()
        Dim example As String = "Example string"
        example.Print()

        example = "Hello"
        example.PrintAndPunctuate(".")
        example.PrintAndPunctuate("!!!!")
    End Sub
End Module

只需这些扩展方法或类似的扩展方法在范围中即可调用这些方法。 如果包含扩展方法的模块在范围内,则它在 IntelliSense 中是可见的,并且可以像普通实例方法一样进行调用。

请注意,调用这些方法时,不会为第一个形参传入任何实参。 前面方法定义中的参数 aString 将绑定到 example,即调用它们的 String 实例。 编译器将使用 example 作为发送给第一个形参的实参。

如果为设置为 Nothing 的对象调用扩展方法,则会执行扩展方法。 这不适用于普通实例方法。 可以在扩展方法中显式检查 Nothing

可扩展的类型

可以对能够在 Visual Basic 参数列表中表示的大多数类型定义扩展方法,包括:

  • 类(引用类型)
  • 结构(值类型)
  • 接口
  • 委托
  • ByRef 和 ByVal 参数
  • 泛型方法参数
  • 数组

由于第一个参数指定扩展方法扩展的数据类型,因此它是必需的,不能是可选的。 因此,Optional 参数和 ParamArray 参数不能是参数列表中的第一个参数。

后期绑定中不考虑扩展方法。 在下面的示例中,语句 anObject.PrintMe() 会引发 MissingMemberException 异常;如果第二个 PrintMe 扩展方法定义被删除,也将引发此异常。

Option Strict Off
Imports System.Runtime.CompilerServices

Module Module4

    Sub Main()
        Dim aString As String = "Initial value for aString"
        aString.PrintMe()

        Dim anObject As Object = "Initial value for anObject"
        ' The following statement causes a run-time error when Option
        ' Strict is off, and a compiler error when Option Strict is on.
        'anObject.PrintMe()
    End Sub

    <Extension()> 
    Public Sub PrintMe(ByVal str As String)
        Console.WriteLine(str)
    End Sub

    <Extension()> 
    Public Sub PrintMe(ByVal obj As Object)
        Console.WriteLine(obj)
    End Sub

End Module

最佳做法

扩展方法提供了一种便捷且功能强大的方法来扩展现有类型。 但是,要成功使用它们,有一些要点需要考虑。 主要是类库的作者需要考虑这些注意事项,但它们可能会影响任何使用扩展方法的应用程序。

通常,与添加到由你控制的类型的扩展方法相比,添加到不属于你的类型的扩展方法更易受攻击。 在不属于你的类中可能会出现很多情况,这些情况可能会影响扩展方法。

  • 如果存在任何可访问的实例成员,它具有与调用语句中的参数兼容的签名,并且不需要从实参到形参的收缩转换,那么将优先使用实例方法而不是任何扩展方法。 因此,如果在某些时候将适当的实例方法添加到类,你依赖的现有扩展成员可能会变得不可访问。

  • 扩展方法的作者无法阻止其他程序员编写存在冲突的可能优先于原始扩展的扩展方法。

  • 可将扩展方法放在各自命名空间中来提高稳定性。 然后,库的使用者可以独立于库的其余部分来包含或排除命名空间,或在命名空间中进行选择。

  • 扩展接口比扩展类更安全,尤其是在没有接口或类时。 对接口进行更改会影响实现它的每个类。 因此,作者在接口中添加或更改方法的可能性较低。 但是,如果类实现的两个接口都具有带相同签名的扩展方法,则两个扩展方法都不可见。

  • 尽可能扩展最具体的类型。 在类型的层次结构中,如果选择一个派生出许多其他类型的类型,则可能会引入实例方法或其他可能会产生干扰的扩展方法。

扩展方法、实例方法和属性

当范围内的实例方法具有与调用语句的参数兼容的签名时,会优先选择实例方法而不是任何扩展方法。 即使扩展方法的匹配度更高,实例方法也具有优先级。 在下面的示例中,ExampleClass 包含一个名为 ExampleMethod 的实例方法,该方法具有一个 Integer 类型的参数。 扩展方法 ExampleMethod 扩展 ExampleClass,并且有一个 Long 类型的参数。

Class ExampleClass
    ' Define an instance method named ExampleMethod.
    Public Sub ExampleMethod(ByVal m As Integer)
        Console.WriteLine("Instance method")
    End Sub
End Class

<Extension()> 
Sub ExampleMethod(ByVal ec As ExampleClass, 
                  ByVal n As Long)
    Console.WriteLine("Extension method")
End Sub

在以下代码中对 ExampleMethod 的第一次调用是调用扩展方法,因为 arg1Long,并且仅与扩展方法中的 Long 参数兼容。 对 ExampleMethod 的第二次调用有一个 Integer 参数 arg2,它调用实例方法。

Sub Main()
    Dim example As New ExampleClass
    Dim arg1 As Long = 10
    Dim arg2 As Integer = 5

    ' The following statement calls the extension method.
    example.exampleMethod(arg1)
    ' The following statement calls the instance method.
    example.exampleMethod(arg2)
End Sub

现在,将两个方法中参数的数据类型反过来:

Class ExampleClass
    ' Define an instance method named ExampleMethod.
    Public Sub ExampleMethod(ByVal m As Long)
        Console.WriteLine("Instance method")
    End Sub
End Class

<Extension()> 
Sub ExampleMethod(ByVal ec As ExampleClass, 
                  ByVal n As Integer)
    Console.WriteLine("Extension method")
End Sub

这次,Main 中的代码两次都调用实例方法。 这是因为 arg1arg2 都可扩大转换为 Long,并且在这两种情况下,实例方法都优先于扩展方法。

Sub Main()
    Dim example As New ExampleClass
    Dim arg1 As Long = 10
    Dim arg2 As Integer = 5

    ' The following statement calls the instance method.
    example.ExampleMethod(arg1)
    ' The following statement calls the instance method.
    example.ExampleMethod(arg2)
End Sub

因此,扩展方法不能替换现有实例方法。 但是,当扩展方法与实例方法同名,但签名不冲突时,这两种方法都可以访问。 例如,如果类 ExampleClass 包含一个不带参数的名为 ExampleMethod 的方法,则允许使用具有相同名称但签名不同的扩展方法,如以下代码所示。

Imports System.Runtime.CompilerServices

Module Module3

    Sub Main()
        Dim ex As New ExampleClass
        ' The following statement calls the extension method.
        ex.ExampleMethod("Extension method")
        ' The following statement calls the instance method.
        ex.ExampleMethod()
    End Sub

    Class ExampleClass
        ' Define an instance method named ExampleMethod.
        Public Sub ExampleMethod()
            Console.WriteLine("Instance method")
        End Sub
    End Class

    <Extension()> 
    Sub ExampleMethod(ByVal ec As ExampleClass, 
                  ByVal stringParameter As String)
        Console.WriteLine(stringParameter)
    End Sub

End Module

此代码的输出如下所示:

Extension method
Instance method

具有属性的情况更简单:如果扩展方法与它扩展的类的属性同名,则扩展方法不可见且无法访问。

扩展方法优先级

当签名相同的两个扩展方法在范围内且可访问时,将调用优先级更高的扩展方法。 扩展方法的优先级基于将方法纳入范围时采用的机制。 以下列表显示了从高到低的优先级层次结构。

  1. 在当前模块中定义的扩展方法。

  2. 在当前命名空间或其任一父命名空间中的数据类型内部定义的扩展方法,子命名空间的优先级高于父命名空间。

  3. 在当前文件中的任何类型导入中定义的扩展方法。

  4. 在当前文件的任何命名空间导入中定义的扩展方法。

  5. 在任何项目级类型导入中定义的扩展方法。

  6. 在任何项目级命名空间导入中定义的扩展方法。

如果优先级不能消除歧义,可使用完全限定的名称来指定要调用的方法。 如果前面示例中的 Print 方法是在名为 StringExtensions 的模块中定义的,则完全限定的名称是 StringExtensions.Print(example) 而不是 example.Print()

请参阅