外部アセンブリの型情報

前回は、外部のソース ファイルをコンパイルしてアセンブリを生成する方法について説明しました。今回はそのアセンブリで定義されているクラスの型情報を取得し、インスタンスを生成したりメソッドなどを利用する方法について説明します。

まずはアセンブリを読み込まなければなりません。コード中のファイル名は適宜置き換えてください。それと、このコードがあるファイルでは Import System.Reflection と宣言してあるものとします。

Dim asm As [Assembly] = [Assembly].LoadFrom("ClsLib.dll")

この場合のアセンブリは DLL に限らず、EXE でも構いません。アセンブリを読み込んだら、内部で定義されているクラスの型情報を得ます。

Dim theClass As Type = asm.GetType("ClsLib.TestClass")

クラスの名前はルートの名前空間も含めて(完全限定名で)指定します。次にメソッドを呼び出したいところですが、Shared でない限りはインスタンスが必要ですので、先にコンストラクタを呼び出します。

Dim ctor As ConstructorInfo = theClass.GetConstructor(New Type() {})

Type の配列には、コンストラクタのパラメータの型を指定します。パラメータが無い場合は、Type の空の配列を渡します。GetConstructor のこの形式では、Public なコンストラクタのみが得られますが、他の形式では Public でないコンストラクタを得ることもできます。ただし、呼び出すときには普通に New TestClass とするのと同じ制限が適用されるため、例えば Private なコンストラクタを TestClass の外部から呼び出すことはできません(例外がスローされます)。

さて、実際にコンストラクタを呼び出すには次のようにします。

Dim obj As Object = ctor.Invoke(New Object() {})

Invoke の戻り値は、生成されたインスタンスです。パラメータなしのコンストラクタを呼び出す場合、Object の配列の替わりに Nothing を渡しても構いません。パラメータがある場合、New Object() {param1, param2} のようにして、Object の配列を渡します。もちろんこれらの型は、GetConstructor のパラメータで指定されたものと一致して(またはキャスト可能になって)いなければなりません。


ようやく、クラスのインスタンスを生成することができたので、次にメソッドを呼び出します。手順はコンストラクタの場合と同様です。

Dim meth As MethodInfo = theClass.GetMethod("DoSomething", New Type() {})

これも 2 番目の引数でパラメータを指定します。オーバーロードがない場合、次のようにしても構いません。

Dim meth As MethodInfo = theClass.GetMethod("DoSomething")

オーバーロードがあると、名前だけではどのメソッドか特定できないので失敗します。実際に呼び出すには次のようにします。

Dim retVal As Object = meth.Invoke(obj, New Object() {})

最初の引数には、クラスのインスタンスを渡します。このメソッドが Shared として宣言されている場合、インスタンスは不要なので Nothing を渡します。Invoke の戻り値は、呼び出したメソッドの戻り値です。戻り値のないメソッドは、Nothing を返します。

ところで、メソッド(およびコンストラクタ)へのこのようなパラメータの渡し方、――つまり New Object() {param} として渡す方法は、場合によっては不具合の原因になることがあります。その場合とは、パラメータが ByRef で渡されるときです。例えばメソッドが次のように定義されているとき、

Sub DoSomething2(ByRef value As Integer)
    value = 100
End Sub

次のコードでは問題が生じます。

Dim meth2 As MethodInfo = theClass.GetMethod("DoSomething2")

Dim v As Integer = 50
meth2.Invoke(obj, New Object() {v})

このコードを書いた人は、おそらく Invoke のあとで v = 100 となっていることを期待したのだと思いますが、実際には v = 50 のままです。理由は宿題にして解決策を示します。

Dim v As Integer = 50
Dim params() As Object = New Object() {v}
meth2.Invoke(obj, params)
v = CInt(params(0))

こうすると Invoke のあとで params(0) が 100 になり、めでたく解決です。不具合の理由がわかったでしょうか?


ついでにプロパティについても触れておくことにします。

Dim prop As PropertyInfo = theClass.GetProperty("TheProp")

Type の配列がありませんが、これは、パラメータを必要とするプロパティが「インデックス付きプロパティ」のみであるため、ほとんどが上記の形式で済むからです。インデックス付きの場合、次のようにして型を指定します。ここでは、パラメータがひとつの Integer であると仮定します。

Dim indexedProp As PropertyInfo = theClass.GetProperty( _
    "TheIndexedProp", New Type() {GetType(Integer)})

プロパティの実体は取得側メソッドと設定側メソッドの組み合わせです。したがって PropertyInfo は、これら二つの MethodInfo を保持しているものと考えられます。実際、GetGetMethod と GetSetMethod を用いれば、それぞれの MethodInfo を得ることができます。そして次のようにして、プロパティの値を得ます。

Dim getMeth As MethodInfo = prop.GetGetMethod()
Dim propVal As Object = getMeth.Invoke(obj, Nothing)

インデックス付きの場合、次のようにして、2 番目のパラメータにインデックスを指定します。

Dim getIndexedMeth As MethodInfo = indexedProp.GetGetMethod()
Dim propVal As Object = getIndexedMeth.Invoke(obj, New Object() {index})

設定側メソッドの場合、2 番目のパラメータに設定する値を指定します。ここでは String 型の値とします。

Dim setMeth As MethodInfo = prop.GetGetMethod()
setMeth.Invoke(obj, New Object() {"Hello"})

インデックス付きの場合、設定する値の前にインデックスを置きます。

Dim setIndexedMeth As MethodInfo = indexedProp.GetSetMethod()
setIndexedMeth.Invoke(obj, New Object() {index, "Hello"})

もし、わざわざ Invoke を呼び出すのが面倒だという場合、PropertyInfo に便利なメソッドが用意されています。GetValue と SetValue です。次のように使います。

Dim propVal As Object = prop.GetValue(obj, Nothing)
prop.SetValue(obj, "HelloWorld", Nothing)

最後のパラメータはインデックスですが、無い場合は Nothing を指定することになっています。


最後に、前回の記事がこっそり修正されていることに気を付けてください。この記事をアップデートした時点で既に修正後の文章になっています。どんな間違いをしていたのかというと、なんか「EXE で定義されているクラスは、外部から参照できない」みたいなことを書いていたのですが、これは実験コードを書いたときに、ルート名前空間の存在を忘れていて参照できなかったのを、仕様上の問題と思い込んでしまったが故の誤解です。すみません。