擴充方法 (Visual Basic)
更新:2007 年 11 月
Visual Basic 2008 加入了擴充方法,可讓開發人員將自訂功能加入至已定義的資料型別中,而不需要建立新的衍生型別 (Derived Type)。擴充方法可讓您撰寫可呼叫的方法,就如同是現有型別的執行個體方法一樣。
備註
擴充方法可能只是 Sub 程序或 Function 程序。您無法定義擴充屬性 (Property)、欄位或事件。所有擴充方法都必須在 System.Runtime.CompilerServices 命名空間 (Namespace) 中,以擴充屬性 (Attribute) <Extension()> 標記。
擴充方法定義的第一個參數會指定方法所擴充的資料型別。執行此方法後,第一個參數就會繫結至叫用 (Invoke) 此方法之資料型別的執行個體。
範例
描述
下列範例會為 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:
Imports ConsoleApplication2.StringExtensions
Module Module1
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(ByVal aString As String)
Console.WriteLine(aString)
End Sub
<Extension()> _
Public Sub PrintAndPunctuate(ByVal aString As String, _
ByVal 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 執行個體。編譯器 (Compiler) 會使用 example 做為傳送至第一個參數的引數。
可以擴充的型別
您可以對大多數型別定義擴充方法 (這些型別可以在 Visual Basic 的參數清單中呈現出來),包括下列各項:
類別 (參考型別)
結構 (實值型別)
介面
委派
ByRef 和 ByVal 引數
泛型方法參數
陣列
由於第一個參數會指定擴充方法要擴充的資料型別,所以此為必要項而不可是選擇項。因此,Optional 參數和 ParamArray 參數不可以是參數清單中的第一個參數。
最佳作法
擴充方法可提供方便且功能強大的方法,讓您擴充現有的型別。不過,若要順利使用這些方法,您必須考量幾個重點。這些考量重點主要是針對類別庫的作者,但也可能影響使用擴充方法的應用程式。
一般來說,您加入至未擁有之型別的擴充方法,會比加入至所控制之型別的擴充方法來得更不安全。在您未擁有的類別中可能會發生干擾擴充方法的情形。
如果存在的可存取執行個體成員都具有與呼叫端陳述式之引數相容的簽章,而沒有從引數到參數所需要的縮小轉換,則擴充方法會偏好使用執行個體方法。因此,如果將適當的執行個體方法加入至類別,您所依賴的現有擴充成員可能變成無法使用。
擴充方法的作者無法避免其他程式設計人員撰寫相衝突的擴充方法,而這些擴充方法的優先順序甚至可能高於原始的擴充方法。
您可以將擴充方法放置在自己的命名空間中,即可增進其強度。您程式庫的消費者接著就能併入或排除命名空間,或者選擇程式庫中其他不同的命名空間。
特別在您未擁有介面或類別時,擴充介面可能比擴充類別更不安全。介面中的變更會影響實作該介面的每個類別。因此,作者較不希望在介面中加入或變更方法。不過,如果類別實作兩個介面,而這兩個介面有簽章相同的擴充方法,就看不到任一個擴充方法。
擴充最特定的型別。在型別階層中,如果選取會從中衍生許多其他型別的型別,則可能會引入執行個體方法的各種層面,或者其他可能與您的擴充方法相衝突的擴充方法。
擴充方法、執行個體方法和屬性
當範圍內的執行個體方法具有與呼叫陳述式之引數相容的簽章時,會先選擇執行個體方法,再選擇任何擴充方法。即使擴充方法更符合,但執行個體方法會優先適用。在下列範例中,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 ConsoleApplication2.ExtensionExample
Module Module1
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
End Module
Imports System.Runtime.CompilerServices
' Define an extension method named ExampleMethod.
Module ExtensionExample
<Extension()> _
Sub ExampleMethod(ByVal ec As ExampleClass, _
ByVal stringParameter As String)
Console.WriteLine(stringParameter)
End Sub
End Module
這個程式碼的輸出如下:
Extension method
Instance method
此情況使用屬性會簡單些:如果擴充方法的名稱與其擴充類別的屬性相同,擴充方法就看不到且無法存取。
擴充方法優先順序
當具有相同簽章的兩個擴充方法都在範圍內而且可存取時,將會叫用具有較高優先順序的擴充方法。擴充方法優先順序是根據用來將方法帶入範圍的機制所決定。下列清單從最高到最低,列出優先順序階層。
在目前的模組內定義的擴充方法。
在目前的命名空間或任何父代之資料型別內定義的擴充方法,其子命名空間的優先順序高於父代命名空間。
在目前檔案之型別匯入內定義的擴充方法。
在目前檔案之命名空間匯入內定義的擴充方法。
在專案層級的型別匯入內定義的擴充方法。
在專案層級的命名空間匯入內定義的擴充方法。
如果優先順序無法解決模稜兩可 (Ambiguity) 的問題,則可以使用完整名稱來指定您正要呼叫的方法。如果之前範例中的 Print 方法是在 StringExtensions 這個模組中定義,則完整名稱為 StringExtensions.Print(example),而不是 example.Print()。