VB のたまご

作成日: 2015/12/19, 更新日: 2015/12/24


リフレクションを勉強して、メタデータを操作しよう

実行ファイルから、メタデータを取り出せるリフレクション

  •  昔の話ですが、一時期、リフレクション( System.Reflection )の楽しさに、はまっていたことがありました。 リフレクションを使うと、メタデータを取り出すことができ、普段とは違う世界にひたることができます。

  •  何が面白いって、dll や exe などの実行ファイルから、ソースコードに書いてある情報を、引っ張り出すことができるんですよ。 (実行ファイルに、ソースコードで定義したものが入っているんですよ!)。って書くと当たり前なんだけど。

  •  プログラム作ってビルドすると、実行ファイルが作成されます。 だから、ソースコードに書いている情報が、dll や exe などの実行ファイルに入っているわけですが、 このことを、実際に見て確認して、「おー、ちゃんと入ってる!」と実感することができます。 今日は、リフレクションについて、見ていきたいと思います。


スポンサーリンク


2つの側面を持つリフレクション(動的実行編)

  •  リフレクションには、大まかに2通りの使い道があります。動的実行とメンバー情報の取得です。 動的実行とは、参照設定しないでプログラムを読み込んで動かす、というものです。 用途としては、プラグイン機能を実現させる手段として、用いることが多いです。

  •  以下のサンプルは、プラグインではありませんが、リフレクションを利用してプログラム実行させています。 2つのプロジェクトを準備していて、1つは WindowsApplication、1つは ClassLibrary と分けて作成しています。

  • ' ClassLibrary/Class1.vb
    Public Class Class1
    
        Public Property Name As String = String.Empty
    
        Public Sub New(name As String)
            Me.Name = name
        End Sub
    
        Public Sub ShowName()
            Console.WriteLine(Me.Name & " と言います。")
        End Sub
    
    End Class
    
    ' WindowsApplication/Form1.vb
    ' 参照設定に、ClassLibrary を追加しています。
    
    Imports ClassLibrary1
    
    ' 通常の使い方
    Dim o1 As New Class1("taro")
    o1.Name = "jiro"
    o1.ShowName()
    
    ' 出力結果
    jiro と言います。
    

  •  普段のプログラミングだと、このように外部プログラムを呼び出して使います。 これが、リフレクションを使うと、参照設定しなくても読み込めるようになります。

  • Imports System.IO
    Imports System.Reflection
    
    ' 参照設定して、プログラム内で使えるようにする準備、と同じイメージ
    Dim dllFile As String = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ClassLibrary1.dll")
    Dim asm As Assembly = Assembly.LoadFrom(dllFile)
    
    ' Class1 のクラス情報を取得
    ' Imports しないで、名前空間付きでクラス名を指定するイメージ
    Dim t As Type = asm.GetType("ClassLibrary1.Class1")
    
    ' Dim o1 As New Class1("taro") と同様
    ' ただし、この場合、o1 は Object 型
    Dim o1 As Object = Activator.CreateInstance(t, New Object() {"taro"})
    
    ' o1.Name = "jiro" と同様
    t.GetProperty("Name").SetValue(o1, "jiro")
    
    ' o1.ShowName() と同様
    t.GetMethod("ShowName").Invoke(o1, Nothing)
    
    ' 他にも。
    ' Dim nameProperty As String = o1.Name と同様
    Dim nameProperty As String = CStr(t.GetProperty("Name").GetValue(o1))
    Console.WriteLine(nameProperty)
    
    ' 型の定義情報を取得してくる担当が、Type 型。
    ' 値の管理、メソッド実行するためのエネルギーを渡す担当が、Object 型(に入れている、インスタンス生成したオブジェクト)
    
    ' 出力結果
    jiro と言います。
    jiro
    

  •  参照設定そのままで、ソースを変更しても OK です。 もしも、参照設定を外した場合は、ClassLibrary.dll はビルド時に自動的にコピーしてこなくなりますので、 実行前に、exe ファイルと同じ場所に、ClassLibrary.dll を配置しておいてください。

  •  ソース中のコメントにも記載していますが、Type 型と Object 型の2つを使って操作していく、というイメージで見ると分かりやすいかと思います。 Type 型変数は、メンバー呼び出しや値の取得、設定するのに使います。Object 型変数には、インスタンス生成したオブジェクトがセットされています。 各値を管理したい時、メソッド実行したい時に、Object 型変数を渡すことで実行することができます。

  •  何をするにもメソッド経由で作りこんでいく、という独特の書き方ではありますが、なんとなくは読める気がしませんか?
  • Dim dllFile As String = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ClassLibrary1.dll")
    Dim asm As Assembly = Assembly.LoadFrom(dllFile)
    Dim t As Type = asm.GetType("ClassLibrary1.Class1")
    Dim o1 As Object = Activator.CreateInstance(t, New Object() {"taro"})
    
    t.GetProperty("Name").SetValue(o1, "jiro")
    t.GetMethod("ShowName").Invoke(o1, Nothing)
    Dim nameProperty As String = CStr(t.GetProperty("Name").GetValue(o1))
    
  •  と思ったけど、コメント外すと見づらくなりますね。リフレクションは慣れです。 何回も書いていると、毎回決められた手順で操作していること、使うメソッドも毎回同じメソッドなので、勝手に慣れてきます。 普段のプログラム組む手順と同様で、1.参照設定のように、プログラムを読み込む。2.クラス名を指定する。 3.メンバー(プロパティやメソッド等)を呼び出したり、値を編集する。という風に、流れ作業で書けるようになります。


スポンサーリンク


2つの側面を持つリフレクション(メンバー取得編)

  •  続いては、メンバー情報の取得についてです。リフレクションを使って、プログラム自体に関する情報を取得することができます。 普段、プログラミングする際、クラスを定義してメンバーを定義して、それをインスタンス生成して使いますよね。データクラスとかにして。 リフレクションを使うと、「データクラス自体のデータ」を取得することができます。 例えば、アクセスレベルは Public だねとか、メンバーに Name プロパティがいるねとか、関連する情報を取得することができます。 この「データクラス自体のデータ」をメタデータと言います。

  •  用途としては、名前付けに関する規約のチェック等の、分析系処理です。 あとは、設計書のメンテナンス用に、一覧で洗い出しておく等、設計書サポート用かな、と思います。

  • Imports System.IO
    Imports System.Reflection
    
    Dim dllFile As String = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ClassLibrary1.dll")
    Dim asm As Assembly = Assembly.LoadFrom(dllFile)
    Dim t As Type = asm.GetType("ClassLibrary1.Class1")
    Dim b As BindingFlags = BindingFlags.Public Or
                            BindingFlags.NonPublic Or
                            BindingFlags.Instance Or
                            BindingFlags.Static Or
                            BindingFlags.DeclaredOnly
    
    ' 全メンバー取得したい場合
    Console.WriteLine("メンバー一覧:")
    For Each member As MemberInfo In t.GetMembers(b)
        Dim typ As String = [Enum].GetName(GetType(Reflection.MemberTypes), member.MemberType)
        Console.WriteLine("{0} : {1}", member.Name, typ)
    Next
    
    ' 個別にメンバー取得したい場合(プロパティの場合)
    Console.WriteLine("プロパティ一覧:")
    For Each pInfo As PropertyInfo In t.GetProperties(b)
        Console.WriteLine("{0} As {1}", pInfo.Name, pInfo.PropertyType)
    Next
    
    ' 出力結果
    メンバー一覧:
    get_Name : Method
    set_Name : Method
    ShowName : Method
    .ctor : Constructor
    Name : Property
    _Name : Field
    プロパティ一覧:
    Name As System.String
    

  •  こんな感じで、メンバー情報を取得できます。 これができるのは、実行ファイルの中の構造が、ソースコードそのままではなく、整理整頓されて分類分けされているからでしょうね。


実行ファイルの解放に注意

  •  実行ファイルを読み込むと、読み込み元のプログラムが終了するまで、実行ファイルは掴まれたままとなります。 テキストファイルを開放するのと同じように、簡単には解放することができません。

  •  使い終わった後で開放するためには、2通りの対処方法があります。 1つ目は、実行ファイルを読み込む専用のプログラムを準備して、間を仲介してもらう構成に変える案です。

  •   メインPG(実行ファイル読み込み)
        ↓
      メインPG ⇔ 仲介PG(実行ファイル読み込み)

  •  この場合、仲介PGに読み込みたい実行ファイルを引数などで渡して、関連データを外部ファイルとして出力します。 仲介PGは、出力処理後に終了して、実行ファイルも解放されます。

  •  2つ目は、アプリケーションドメインを別に作成して、そちらで実行ファイルを読み込むという案です。 1exeファイルにつき1プロセスで1アプリケーションドメインなのですが、別のアプリケーションドメインを作成する命令を使用することで、 1つ目と同様の効果を得ることができます。

  •  もちろん、つかまったままでもいい場合は、考える必要はありません。 これらを考えると、リフレクションは、開発支援ツール向きの技術なのかなと思います。

  • 【追記】関連記事を書きました。詳しく知りたい方は、こちらも合わせてご覧ください。
  • アプリケーションドメインについて
  • exeファイルを起動したまま、参照プログラムを開放したい

  • 【追記2】リフレクションの続きを書きました。各メンバーについて、それぞれ専用のクラスを使用した取得方法を記載しています。
  • リフレクションについて2

  •  最後までこの記事を読んでいただき、ありがとうございました。


スポンサーリンク