C#リフレクション:メタデータの動的操作と実装パターン

リフレクションの概要

リフレクション(Reflection)は、プログラムの実行中にメタデータを検査し、型情報に基づいてオブジェクトを操作する機能です。System.Reflection名前空間を使用することで、コンパイル時に型が不明であったり、外部DLLに含まれているクラスに対して、動的にインスタンスを作成したり、メソッドを呼び出したり、プロパティにアクセスしたりすることが可能になります。これにより、高度な柔軟性を持ったアーキテクチャ、例えば依存関係の注入やプラグインシステムの実装が容易になります。

主な操作対象となるクラスには以下のようなものがあります。

  • Assembly: アセンブリ(DLLやEXE)全体をロードし、含まれるモジュールや型を管理します。
  • Module: アセンブリ内のモジュール情報を取得します。
  • Type: クラス、インターフェース、構造体などの型宣言を表します。
  • MethodInfo / PropertyInfo / FieldInfo: それぞれメソッド、プロパティ、フィールドの詳細情報へのアクセスを提供します。

アセンブリの読み込み方法

リフレクションを行うには、まず対象のアセンブリを読み込む必要があります。Assemblyクラスには、アセンブリを読み込むためのいくつかのメソッドが存在し、それぞれ動作と用途が異なります。

1. Assembly.Load
アセンブリの「表示名」(Assembly Name)を指定して読み込みます。このメソッドは、グローバルアセンブリキャッシュ(GAC)またはアプリケーションのベースディレクトリ(binフォルダなど)を検索対象とします。最も標準的な読み込み方法です。

// アセンブリの表示名を指定して読み込み(binディレクトリ内またはGACから検索)
var targetAssembly = Assembly.Load("MyUtilityLibrary");
Console.WriteLine($"Loaded: {targetAssembly.FullName}");

2. Assembly.LoadFrom
ファイルパスを指定してアセンブリを読み込みます。指定されたDLLおよび、そのDLLが参照している依存アセンブリも自動的に読み込まれます。同じIDを持つアセンブリが異なるパスにある場合、LoadFromは最初に読み込まれたものを返します。

// ファイルパスを指定して読み込み
var path = @"C:\Projects\MyApp\Bin\MyUtilityLibrary.dll";
var assemblyFromPath = Assembly.LoadFrom(path);
Console.WriteLine($"Loaded from path: {assemblyFromPath.Location}");

3. Assembly.LoadFile
これもファイルパスを指定しますが、LoadFromとは異なり、依存関係にあるアセンブリは自動的に読み込まれません。また、パスが異なっていても、同じアセンブリであれば複数回読み込むことが可能です。 isolatedな環境で特定のDLLを扱う場合に利用されます。

型情報の取得とメンバの探索

アセンブリを読み込んだ後は、そこに含まれる型やメンバを調査できます。GetTypesメソッドですべての型を取得し、Typeクラスのメソッドを使ってメンバ情報を抽出します。

var assembly = Assembly.Load("MyUtilityLibrary");
Type[] definedTypes = assembly.GetTypes();

foreach (var type in definedTypes)
{
    Console.WriteLine($"Type: {type.FullName}");

    // パブリックなメソッドのみを取得(継承元のメソッドを除外)
    var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
    foreach (var method in methods)
    {
        Console.WriteLine($"  Method: {method.Name}");
    }

    // フィールドの取得
    var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
    foreach (var field in fields)
    {
        Console.WriteLine($"  Field: {field.Name} ({field.FieldType.Name})");
    }
}

ここではBindingFlagsを使用して、取得するメンバの範囲(パブリックのみ、プライベートを含むか、継承元を含むかなど)をフィルタリングしています。

実践例:設定ファイルによる動的インスタンス化

リフレクションの典型的な活用例として、設定ファイルに基づいて具象クラスを切り替える「依存関係の逆転」の実装があります。データ処理のインターフェースを定義し、実装クラスを設定ファイルで動的に変更するシナリオを考えます。

まず、共通のインターフェースと実装クラスを含むプロジェクト(クラスライブラリ)を想定します。

IExporter.cs (インターフェース)

namespace DataProcessor
{
    public interface IExporter
    {
        void Execute(string data);
    }
}

CsvExporter.cs (実装クラス1)

namespace DataProcessor
{
    public class CsvExporter : IExporter
    {
        public void Execute(string data)
        {
            Console.WriteLine($"[CSV Export] {data} をCSV形式で出力しました。");
        }
    }
}

JsonExporter.cs (実装クラス2)

namespace DataProcessor
{
    public class JsonExporter : IExporter
    {
        public void Execute(string data)
        {
            Console.WriteLine($"[JSON Export] {data} をJSON形式で出力しました。");
        }
    }
}

次に、呼び出し元のコンソールアプリケーション(.NET Framework)の設定ファイルApp.configで、使用する実装クラスの完全修飾名を定義します。

App.config

<configuration>
  <appSettings>
    <!-- 完全修飾名(名前空間.クラス名)を指定 -->
    <add key="ExporterType" value="DataProcessor.JsonExporter"/>
  </appSettings>
</configuration>

最後に、メインプログラムでリフレクションを使用してインスタンスを生成し、メソッドを実行します。

using System;
using System.Configuration;
using System.Reflection;
using DataProcessor; // インターフェースの参照が必要

namespace ReflectionClient
{
    class Program
    {
        static void Main(string[] args)
        {
            // 1. アセンブリのロード
            // ※ 実際には参照設定に追加されているか、適切なパスにある必要があります
            var targetAssembly = Assembly.Load("DataProcessor");

            // 2. 設定ファイルからクラス名を取得
            string configClassName = ConfigurationManager.AppSettings["ExporterType"];

            // 3. 型情報を取得
            Type exporterType = targetAssembly.GetType(configClassName);
            if (exporterType == null)
            {
                Console.WriteLine("指定された型が見つかりません。");
                return;
            }

            // 4. インスタンスの動的生成とインターフェースへのキャスト
            IExporter exporter = (IExporter)Activator.CreateInstance(exporterType);

            // 5. メソッドの実行
            exporter.Execute("売上データ");
            
            Console.ReadKey();
        }
    }
}

このコードを実行すると、設定ファイルのExporterTypeの値に応じてCsvExporterまたはJsonExporterがインスタンス化されます。例えば、valueDataProcessor.CsvExporterに変更して再ビルドすることなく、設定ファイルだけ書き換えるだけで出力形式をCSVからJSONへ切り替えることができます。これにより、メインロジックを修正することなく、機能の追加や変更が可能な保守性の高い設計が実現できます。

タグ: C# System.Reflection .NET Framework Assembly Dependency Injection

7月9日 17:13 投稿