拡張メソッド (Visual Basic)
更新 : 2007 年 11 月
Visual Basic 2008 では拡張メソッドが導入されました。拡張メソッドを使用すれば、新しい派生型を作成しなくても、既に定義されているデータ型にカスタム機能を追加することが可能になります。拡張メソッドの機能によって、既存の型のインスタンス メソッドを呼び出す場合と同じ要領で呼び出せるメソッドを作成できるようになりました。
解説
拡張メソッドになるのは、Sub プロシージャと Function プロシージャだけです。拡張プロパティ、拡張フィールド、拡張イベントを定義することはできません。すべての拡張メソッドは、System.Runtime.CompilerServices 名前空間の拡張属性 <Extension()> でマークする必要があります。
拡張メソッド定義の最初のパラメータでは、そのメソッドが拡張するデータ型を指定します。メソッドが実行されると、最初のパラメータは、そのメソッドを呼び出すデータ型のインスタンスにバインドされます。
例
説明
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 の拡張ですが、今回は 2 つのパラメータを定義します。最初のパラメータ aString では、この拡張メソッドによって String を拡張することを指定します。2 番目のパラメータ 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 が、メソッドを呼び出す String のインスタンスである example にバインドされています。コンパイラは、最初のパラメータに渡す引数としてその example を使用します。
拡張可能な型
拡張メソッドは、Visual Basic のパラメータ リストで記述できるほとんどの型で定義できます。以下に例を示します。
クラス (参照型)
構造体 (値型)
インターフェイス
デリゲート
ByRef 引数と ByVal 引数
ジェネリック メソッド パラメータ
配列
最初のパラメータでは、メソッドによって拡張するデータ型を指定するので、最初のパラメータは必須であり、任意指定にすることはできません。したがって、パラメータ リストの最初のパラメータとして、Optional パラメータと ParamArray パラメータを記述することはできません。
最適な使用方法
拡張メソッドは、既存の型を拡張するための便利で強力な手段になります。それでも、適切に使用するにはいくつかの注意点があります。ここで取り上げる注意点は、主にクラス ライブラリを作成するときに当てはまりますが、拡張メソッドを使用するアプリケーションであればどんなアプリケーションにも影響する可能性があります。
一般的に、自分で所有していない型に追加した拡張メソッドは、自分で制御できる型に追加した拡張メソッドよりも脆弱になります。自分で所有していないクラスでは、拡張メソッドの動作に影響を及ぼしかねない事柄がいくつか発生する可能性があります。
呼び出し元ステートメントの引数との互換性があるシグネチャを持ったアクセス可能なインスタンス メンバが存在し、引数からパラメータへの縮小変換が不要な場合は、拡張メソッドよりもそのインスタンス メソッドの方が優先的に使用されます。したがって、該当するインスタンス メソッドがいずれかの時点でクラスに追加されると、使用しなければならない既存の拡張メソッドにアクセスできなくなる可能性があります。
拡張メソッドの作成者の側では、その拡張メソッドよりも優先的に使用される可能性がある別の拡張メソッドを他のプログラマが作成する、という事態を防止できません。
拡張メソッドをそれ自身の名前空間に入れておけば、拡張メソッドの信頼性が向上します。ライブラリを利用する側では、ライブラリの名前空間とそれ以外の部分を分けて、名前空間を組み込んだり除外したり取捨選択したりすることができます。
クラスを拡張するよりもインターフェイスを拡張する方が安全です。インターフェイスまたはクラスを自分で所有していない場合は特にそういえます。インターフェイスが変更されると、そのインターフェイスを実装するすべてのクラスが影響を受けます。したがって、インターフェイスでメソッドが追加されたり変更されたりする可能性の方が低いといえます。ただし、クラスが同じシグニチャの拡張メソッドを持つ 2 つのインターフェイスを実装する場合、どちらの拡張メソッドにもアクセスできません。
できるだけ具体性の高い型を拡張するようにします。型の階層の中で他の多くの型の派生元になっている型で拡張メソッドを選択すると、その拡張メソッドの動作に影響を及ぼしかねないインスタンス メソッドや他の拡張メソッドが組み込まれる可能性が高くなります。
拡張メソッド、インスタンス メソッド、およびプロパティ
スコープ内のインスタンス メソッドが、呼び出し元ステートメントの引数と互換性があるシグネチャを持っている場合、拡張メソッドよりもそのインスタンス メソッドの方が優先的に使用されます。この場合、より適合する拡張メソッドがあっても、インスタンス メソッドの方が優先されます。次の例では、ExampleClass に、Integer 型のパラメータを 1 つ持つ ExampleMethod という名前のインスタンス メソッドが含まれています。拡張メソッド ExampleMethod は ExampleClass を拡張し、Long 型のパラメータを 1 つ持ちます。
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 の 2 回目の呼び出しでは、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
次は、2 つのメソッド間でパラメータのデータ型が逆になっています。
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
プロパティの場合、状況はより単純です。拡張メソッドの名前がそのクラスのプロパティと同じである場合、その拡張メソッドは非表示になり、アクセスできません。
拡張メソッドの優先順位
2 つの拡張メソッドのシグネチャが同じで、そのいずれもスコープに入っていてアクセスが可能な場合は、優先順位の高い方が呼び出されます。拡張メソッドの優先順位は、メソッドをスコープに組み入れる方法に基づいています。優先順位の高い方から低い方へと並べると、次のようになります。
現在のモジュールの中で定義されている拡張メソッド。
現在の名前空間の方がその親に相当する名前空間よりも優先順位が高ければ、現在の名前空間またはそのいずれかの親に相当する名前空間にあるデータ型の中で定義されている拡張メソッド。
現在のファイルの型インポートの中で定義されている拡張メソッド。
現在のファイルの名前空間インポートの中で定義されている拡張メソッド。
プロジェクト レベルの型インポートの中で定義されている拡張メソッド。
プロジェクト レベルの名前空間インポートの中で定義されている拡張メソッド。
優先順位を適用してもあいまいさが残る場合は、完全修飾名を使用して、呼び出すメソッドを指定できます。先ほどの例の Print メソッドが StringExtensions という名前のモジュールで定義されていれば、完全修飾名は example.Print() ではなく StringExtensions.Print(example) になります。