Cisis - C##

ASP.NET Core 開発者のブログ

ジェネリックメソッドをリフレクションで呼び出す

重要キーワード

  • MakeGenericMethod メソッド

はじめに

ASP.NET Core の開発では、いろいろな型に対して同じメソッドを繰り返し実行したい場合がまれにあります。たとえば最近では DI が当たり前になっていますが、 IServiceCollection の拡張メソッド群は引数は何も取らずに型引数だけ指定することも多いです。 このとき、毎回 AddSingleton<T>Configure<T> を手書きするのではなく、型の配列に対してループで実行できたらいいなと思うことがあります。

今回は、このような用途のためにジェネリックメソッドをリフレクションで扱う方法について説明します。

非ジェネリックメソッドの呼び出し

Type を引数にとる非ジェネリックメソッドの場合は、下記のように書けます。

void Main()
{
    var types = new[] {typeof(string), typeof(int), typeof(decimal)};
    foreach (var type in types) 
    {
        Add(type);
    }
}

// 型に対してなにかする関数
static void Add(Type type) {var types = new Type[]()

ジェネリックメソッドの呼び出し

では、以下のようなメソッドの定義ではどうでしょうか。

public class SampleClass
{
    // 型の名前をコンソールに出すジェネリックメソッド
    public static void StaticMethod<T>() 
    {
        Console.WriteLine(typeof(T).FullName);
    }
    
    public void InstanceMethod<T>(string prefix) 
    {
        Console.WriteLine(prefix + typeof(T).Name);
    }
}

このクラスのメソッドは Type 引数を受け取らず、ジェネリック版のみ提供されていますので、上記のような簡単なループで実装することはできません。 このような場合は、リフレクションを使って次のようにします。

void Main()
{
    var types = new[] {typeof(string), typeof(int), typeof(decimal)};
    
    // StaticMethod メソッド(型引数が埋まっていない)を取得
    var staticMethodInfo = typeof(SampleClass)
        .GetMethod(name: nameof(SampleClass.StaticMethod), 
            genericParameterCount: 1, // 型引数の数を指定
            bindingAttr: BindingFlags.Public | BindingFlags.Static, // public static なメンバを検索
            binder: null, 
            types: Array.Empty<Type>(),
            modifiers: null);
            
    // InstanceMethod メソッド(型引数が埋まっていない)を取得
    var instanceMethodInfo = typeof(SampleClass)
        .GetMethod(name: nameof(SampleClass.InstanceMethod), 
            genericParameterCount: 1, // 型引数の数を指定
            bindingAttr: BindingFlags.Public | BindingFlags.Instance, // public なメンバを検索
            binder: null,
            types: new[] { typeof(string)}, // 引数の型の配列。
            modifiers: null);
            
    var instance = new SampleClass();
    
    foreach (var type in types) 
    {
        staticMethodInfo!.MakeGenericMethod(type) // 型引数を指定してジェネリックメソッドを作る
            .Invoke(null, null); // 実行。 引数なしの static メソッドなので引数は両方 null

        instanceMethodInfo!.MakeGenericMethod(type) // 型引数を指定してジェネリックメソッドを作る
            .Invoke(instance, new object[] {"prefix string "}); // 実行。 引数ありのインスタンスメソッドなので、第1引数にインスタンス、第2引数に引数の配列
    }
}

まず、対象のメソッドが定義されている型の Type インスタンスを取得します。 ここでは typeof 演算子で取得しています。 次に、Type.GetMethod メソッドを使用して呼び出したいメソッドの MethodInfo を取得します。 このとき、ジェネリック型引数が埋まっていない中途半端な状態の MethodInfo が返ってくるため、MakeGenericMethod メソッドで型引数をきちんと埋めてあげましょう。

型引数が埋まった MethodInfo がとれれば、あとは非ジェネリックメソッドと同じように Invoke に引数を渡して実行すれば OK です。

少し複雑な呼び出しテクニック

これでめでたくジェネリックメソッドの呼び出しができたわけですが、もう少し複雑で長い呼び出しが必要になった場合はどうでしょうか? メソッドを 2 つ呼び出すだけでも十分ややこしく、読みにくいコードなのに、呼び出すメソッドがもっと増えて、戻り値や引数の処理(すべて object 型で扱います!)まで入ってきたら、もうほとんど書いた本人以外わからないコードになってしまいます。

そこで、次のようなテクニックをおすすめしたいと思います。

  • まず一連の処理を書いた ジェネリックメソッド を一つ作る
  • リフレクションを使って、そのジェネリックメソッドを一つだけ呼び出す

この方法で先程の例を書き直してみると、次のようになります。

void Main()
{
    var types = new[] {typeof(string), typeof(int), typeof(decimal)};
    
    // InvokeMultipleMethods を呼び出す MethodInfo
    var instanceMethodInfo = typeof(SampleClass)
        .GetMethod(name: nameof(SampleClass.InstanceMethod), 
            genericParameterCount: 1, // 型引数の数を指定
            bindingAttr: BindingFlags.Public | BindingFlags.Instance, // public なメンバを検索
            binder: null,
            types: new[] { typeof(string)}, // 引数の型の配列。
            modifiers: null);
            
    var instance = new SampleClass();
    
    foreach (var type in types) 
    {
        instanceMethodInfo!.MakeGenericMethod(type) // 型引数を指定してジェネリックメソッドを作る
            .Invoke(instance, new object[] {"prefix string "}); // 実行。 引数ありのインスタンスメソッドなので、第1引数にインスタンス、第2引数に引数の配列
    }
}

public class SampleClass
{
    // このメソッドに処理を書いておいて呼び出す
    public void InvokeMultipleMethods<T>(string prefix)
    {
        // ここは普通のメソッド呼び出しなので普通に読める
        StaticMethod<T>();
        this.InstanceMethod<T>(prefix);
    }
    
    // 型に対してなにかするジェネリックメソッド
    public static void StaticMethod<T>() 
    {
        Console.WriteLine(typeof(T).FullName);
    }
    
    public void InstanceMethod<T>(string prefix) 
    {
        Console.WriteLine(prefix + typeof(T).Name);
    }
}

さきほどよりもスッキリしたのではないでしょうか。 このようにすることで、リフレクション回数を減らし、可読性も上げることができます。

適用シーンと他の手法

今回のようなリフレクションを用いた手法は、「起動時に一回だけすべての型に対して行う」ような場合に威力を発揮します。 DI の設定などはこれにあたります。 実行中に何度も呼ばれるような箇所にリフレクションを使ってしまうと、かなり低速なコードになってしまいます。 逆に、起動時に一回だけであればリフレクションの遅さもあまり気にならず、また非常に手軽なメタプログラミングの方法として重宝するでしょう。

他に同様のことができるものとしては、コード生成という手法があります。 コード生成にも以下のようにいろいろと種類があります。 近年は SourceGenerator が登場し、静的コード生成の選択肢が広がっています。

  • 動的コード生成(実行時にコード生成、型情報はリフレクションをもとに解析して使う)
    • IL
      • IL(.NET における中間言語)を直接書いて、実行時にプログラムを動的に作り出して実行
      • ○ 一度生成してしまえば高速
      • ○ C# では書けないような高度な操作も可能
      • ☓ IL は読み書きの難易度が非常に高い
    • Expression Tree
      • 式木というものを用いてプログラムを動的に生成する
      • ○ 一度生成すれば高速
      • △ IL よりはお手軽だが、それなりに学習コストもあり制限もある
  • 静的コード生成(ビルド時やコードを書くときにコード生成)
    • T4
      • ○ 古くからある安定した技術
      • ○ T4 という独自の記法でテンプレートを書き、テキストファイルを生成
      • ○ テキストファイルであればなんでも (.cs ファイルも)生成でき、ビルド対象にもできる
      • ○ 事前に生成するものがわかっている場合に便利
      • △ ユーザが T4 を書く必要があるため、広く配布するには若干不向き
    • SourceGenerator
      • ○ 最新の手法
      • ○ ビルド処理に介入、コンパイラが使う構文木(プログラムの構文を解析して木にしたもの)を自由に読み取って、それをもとにコードを生成できる
      • ○ ユーザに余計なことを気にさせず、使い勝手の良いコード生成ができる
      • ○ コードを書いた瞬間にインクリメンタルにコード生成が可能なので使い心地が良い
      • ○ アナライザと連携して高度な安全性や書き心地を提供できる
      • ○ 普通に C# で書いた処理と実行速度が変わらない
      • ☓ 構文木を正しく読み取って処理する必要があるが、ドキュメントが少なく難易度が高い
      • ☓ ちょっとしたことをするには実装コストが高い

最後は一般的な話になってしまいましたが、メタプログラミング手法にも様々なものがあり、それぞれに向き不向きがありますから、場面に応じた最適な手法を選択することが大切です。

静的なコード生成では一般にパフォーマンスの低下は一切見られず、また生成されたコードは誰でも読めるため、メンテナンス性も高いです。しかし、コード生成のための追加の作業をユーザに要求したり、ビルドパフォーマンスに影響を与える場合があります。また、やはり静的な生成では実現できないこともあります。

それに対して、リフレクションや動的生成は非常に強力な手法ですが、参照がほとんど追えなかったり、カプセル化を破壊できるという点で、使い方を誤ればコードベースの品質を破滅的に低下させる要因にもなり得ます。また、よくない実装によって実行速度を低下させてしまう可能性もあります。

特にチーム開発においては、全員にわかりやすく、安全にメンテできることが大前提となることが多いです。無理に難しい手法を導入すると、逆に生産性を落とすことにもつながります。レベルアップのためにはエレガントな手法の追求も大切ですが、ときには愚直に書くことを選択する勇気を持つことも大切です。