擴充方法 (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(".") 來呼叫

下列範例顯示已定義並呼叫 PrintPrintAndPunctuateSystem.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()

另請參閱