扩展方法使开发人员能够将自定义功能添加到已定义的数据类型,而无需创建新的派生类型。 扩展方法可以编写一个可以调用的方法,就像它是现有类型的实例方法一样。
注解
扩展方法只能是 Sub
过程 或 Function
过程。 不能定义扩展属性、字段或事件。 所有扩展方法都必须使用命名空间中的<Extension>
扩展属性System.Runtime.CompilerServices进行标记,并且必须在模块中定义。 如果扩展方法在模块外部定义,则 Visual Basic 编译器将生成错误 BC36551“扩展方法只能在模块中定义”。
扩展方法定义中的第一个参数指定方法扩展的数据类型。 运行该方法时,第一个参数将绑定到调用该方法的数据类型的实例。
该Extension
属性只能应用于 Visual Basic Module
或 Sub
Function
。 如果将其应用于 Class
或 Structure
,Visual Basic 编译器将生成错误BC36550,“‘Extension’属性只能应用于‘Module’、‘Sub’或‘Function’声明”。
示例:
以下示例定义 Print
数据类型的 String 扩展。 该方法使用 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(".")
以下示例演示定义 Print
和调用 PrintAndPunctuate
。
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
的调用是对扩展方法的调用,因为arg1
是Long
,并且仅与扩展方法中的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
两次调用实例方法。 这是因为 `arg1
` 和 `arg2
` 都可以扩大转换为 `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
属性的情况更简单:如果扩展方法的名称与它扩展的类的属性同名,则扩展方法不可见且无法访问。
扩展方法优先级
如果两个具有相同签名的扩展方法在作用域中且可访问,则会调用优先级较高的扩展方法。 扩展方法的优先级基于用于将方法引入作用域的机制。 以下列表显示优先级层次结构,从高到低。
在当前模块中定义的扩展方法。
在当前命名空间或任何父命名空间的数据类型内定义的扩展方法,子命名空间的优先级高于父命名空间。
在当前文件中的任何类型导入中定义的扩展方法。
在当前文件中的任何命名空间导入中定义的扩展方法。
在任何项目级类型导入中定义的扩展方法。
在任何项目级命名空间导入中定义的扩展方法。
如果优先级不解析歧义,则可以使用完全限定的名称来指定要调用的方法。 如果前面示例中的Print
方法定义在名为StringExtensions
的模块中,那么完全限定名称是StringExtensions.Print(example)
而不是example.Print()
。