LoginSignup
2
0

More than 1 year has passed since last update.

C#で型名もメソッド名も引数も文字列で指定して呼び出す

Last updated at Posted at 2022-12-01

この記事はPONOS Advent Calendar 2022の2日目の記事です。
昨日は@r73ryoさんのUnityエディタ上で循環的複雑度(CCN)を計測するでした。


目的

namespace SampleNamespace
{
    public class SampleClass
    {
        // SampleMethod(1, 23.4) -> "(1, 23.4)"
        public static string SampleMethod(int a, float b)
        {
            return $"({a}, {b})";
        }
    }
}

というクラスとメソッドがあったとして

var result = Invoker.Invoke("SampleNamespace.SampleClass", "SampleMethod", "42", "1.23");
// result == "(42, 1.23)";

のように呼び出せるようにするための方法です。
コンソールから入力したメソッドを呼びたいときなんかに使えると思います。

注意

今回の解説の実装は呼び出すメソッドがpublicかつstaticであることが前提なのでそれ以外となると工夫が必要です。
また例外処理などは組み込んでいないため利用する際は注意してください。

実装

先にInvorker全体の実装を書いておき、要素をバラしてから解説を行います。

public static class Invoker
{
    /// <summary>
    // 参照しているアセンブリを含めたすべての型のキャッシュ
    /// </summary>
    static readonly TypeInfo[] AllTypes = GetAllTypes();

    /// <summary>
    /// 型名、メソッド名、引数を文字列で指定して呼び込む
    /// </summary>
    /// <param name="typeName">型名。フルパスもしくは型名だけの文字列</param>
    /// <param name="methodName">メソッド名/param>
    /// <param name="args">引数に渡す文字列</param>
    /// <returns>メソッドの実行結果</returns>
    [return: System.Diagnostics.CodeAnalysis.MaybeNull]
    public static object Invoke(string typeName, string methodName, params string[] args)
    {
        var typeInfo = AllTypes.FirstOrDefault(e => e.FullName == typeName) ?? AllTypes.First(e => e.Name == typeName);

        var methods = typeInfo.GetMethods(BindingFlags.Static | BindingFlags.Public);
        var methodInfo = methods.First(e => e.Name == methodName);

        var parameterTypes = methodInfo.GetParameters().Select(e => e.ParameterType).ToArray();
        var parameters = args.Select((arg, i) =>
        {
            var converter = TypeDescriptor.GetConverter(parameterTypes[i]);
            return converter.ConvertFrom(arg);
        }).ToArray();

        return methodInfo.Invoke(null, parameters);
    }

    /// <summary>
    // 参照しているアセンブリを含めたすべての型を取得するメソッド
    /// </summary>
    /// <returns>参照しているアセンブリを含めたすべての型</returns>
    static TypeInfo[] GetAllTypes()
    {
        // 実行中のアセンブリを取得、
        // 参照先のアセンブリもすべて取得して実行中のアセンブリも合わせてコレクションにする
        // すべてのアセンブリのすべての型情報を配列化して返す
        var executingAssembly = Assembly.GetExecutingAssembly();
        var allAssemblies = executingAssembly.GetReferencedAssemblies().Select(e => Assembly.Load(e)).Append(executingAssembly);
        return allAssemblies.SelectMany(assembly => assembly.GetTypes()).Select(e => e.GetTypeInfo()).ToArray();
    }
}

使用例

public static void Main()
{
    var result = Invoker.Invoke("SampleNamespace.SampleClass", "SampleMethod", "42", "1.23");
    Console.WriteLine(result);
}

出力

(42, 1.23)

解説

それでは解説です。

型情報を取得する

型名から型情報を取得しようと思うと参照可能なすべての型を取得する必要があります。
そのためには一度参照先を含めすべてのアセンブリを取得する必要があります。

すべてのアセンブリを取得

まず実行中のアセンブリだけであれば

System.Reflection.Assembly.GetExecutingAssembly();

で取得することができます。
しかしそれでは取得したい型が別アセンブリで定義されていた場合に参照できません。
そこで下記のように実行中のアセンブリの情報から参照先のアセンブリをすべて取得して実行中のアセンブリと合わせてコレクションにします。

// ファイルの頭でSystem.ReflectionとSystem.Linqをusingしている
var executingAssembly = Assembly.GetExecutingAssembly();
var referencedAssemblies = executingAssembly.GetReferencedAssemblies().Select(e => Assembly.Load(e));

// 実行中のアセンブリと参照しているアセンブリを合わせたコレクション
var allAssemblies = referencedAssemblies.Append(executingAssembly);

すべての型情報を取得

単一のアセンブリから型情報をすべて取得するには下記のようにGetTypes()を呼び出すことで取得できます。

var typeInfoArray = assembly.GetTypes();

Assemblyの配列がある場合はの下記ようにすることで参照先のアセンブリを含めたすべての型情報を取得することができます。

allAssemblies.SelectMany(assembly => assembly.GetTypes()).Select(e => e.GetTypeInfo()).ToArray();

InvorkerクラスではGetAllType()でこの結果を取得しAllTypesというstaticメンバにキャッシュしています。
GetAllTypes()の実装と解説のコードは異なりますが短くしているだけで結果は同じです。

型情報を取得する

すべての型情報を取得できているのでその中から指定した名前を持つクラスを見つけるのはTypeInfo.FullNameもしくはTypeInfo.Nameが一致するクラスを取得するだけで済みます。
下記の処理ではFullNameに一致する型名がなかった場合にNameに一致する型を取得するようにしています。

var typeInfo = AllTypes.FirstOrDefault(e => e.FullName == typeName) ?? AllTypes.First(e => e.Name == typeName);

ちなみにFullNameNameの違いはいくつかありますが今回はnamespace名を含んでいるかどうかくらいの認識で大丈夫です。

メソッドの情報を取得する

メソッド呼び出しと引数の情報を取得するには後述するMethodInfoが必要なのでTypeInfoから指定した名前のMethodInfoを取得します。
GetMethods()メソッドにはBindingFlagを複数指定することができます。今回はBindingFlag.Public | BindingFlag.Staticを指定してpublic staticメソッドをすべて取得し、その中から名前が一致するメソッドを取得します。

var methods = typeInfo.GetMethods(BindingFlags.Static | BindingFlags.Public);
var methodInfo = methods.First(e => e.Name == methodName);

注意
型の中に同じ名前のメソッドがある場合は最初に見つけたものがヒットします。

メソッド呼び出し

MethodInfoについて

Invoker.Invokeの最後に使用しているMethodInfo.Invokeについてです。
ドキュメント

C#にはSystem.Reflection.MethodInfoという型が存在し、この型のインスタンスを取得することでメソッドのメタデータにアクセスできるようになります。MethodInfo.Invokeを使用することでそのメソッドを呼び出すことが可能で、その場合メソッドの結果はobject型で返ってきます。
invokeの定義は

Invoke(Object, Object[])

となっており、引数には(呼び出しの対象となるオブジェクト, Invokeで呼ぶメソッドの引数の配列)を渡す必要があります。staticメソッドの場合は第一引数はnullで大丈夫です。

// Invokeの例
public static class App
{
    public static int Add(int a, int b)
    {
        return a + b;
    }

    public static void Main()
    {
        var methodInfo = typeof(App).GetMethod("Add"); // App型から"Add"という名前のメソッドの情報を取得する
        var result = methodInfo.Invoke(null, new object[] { 1, 2 }); // (1, 2)を引数にAddを呼び出す
        Console.WriteLine(result); // 3が出力される
    }

文字列から引数に渡すパラメーターに変換する

引数の型情報を取得する

MethodInfo.Invokeでメソッド呼び出しができますが、渡してやる引数は型が正しい必要があります。そのため下記のように文字列から引数の型に変換したobject[]の変数を生成する必要があります。

// argsはstring[]型で{ "4", "1.23" }が格納されている
// このままだと引数の型が異なるため例外が発生する
methodInfo.Invoke(null, args);

object[] convertedArgs = /* argsをstring[]からobject[] { int(4), float(1.23) } に変換する */;
methodInfo.Invoke(null, convertedArgs);

それを実現するにはMethodInfo.GetParameters()TypeConverterクラスを使用します。

まずメソッドの引数の型ですがGetParameters()を使用することで引数の型情報のメタ情報の配列を取得することができるのでメタ情報からParameterTypeを取得することで引数の型情報の配列を取得することができます。

var parameterTypes = methodInfo.GetParameters().Select(e => e.ParameterType).ToArray()

補足
MethodInfo.GetParamters()は引数の情報をAttributeなども含めてすべて返しますが、今回必要なのは型の情報だけなのでSelectParamaterType(TypeInfo)のコレクションに変換しています。

文字列から引数の型に変換する

System.ComponentModel.TypeDescriptor.GetConverter(TypeInfo)というメソッドを使用することで何らかの値を指定の型に変換するためのTypeConverterを取得することができます。TypeConverter.ConvertFrom(object)を使用することで文字列を指定の型に変換することができます。

var typeInfo = typeof(int);
var converter = TypeDescriptor.GetConverter(typeInfo);
var v = converter.ConvertFrom("42"); // vはint型で42

これを使用してすべての文字列をメソッドの引数の型に変換してやることでInvokeに渡す配列を生成することができます。

var parameters = args.Select((arg, i) =>
{
    var converter = TypeDescriptor.GetConverter(parameterTypes[i]);
    return converter.ConvertFrom(arg);
}).ToArray();

Invoke

呼び出したいメソッドのMethodInfoと引数のオブジェクトの配列があるのであればメソッド呼び出しは下記のようにInvokeを呼び出してやるだけで良いです。

methodInfo.Invoke(null, parameters);

Invoker.Invoke

ここまでのことを合わせるとInvoker.Invokeの実装はこのようになります

public static object Invoke(string typeName, string methodName, params string[] args)
{
    var typeInfo = AllTypes.FirstOrDefault(e => e.FullName == className) ?? typeInfo = AllTypes.First(e => e.Name == className);

    var methods = typeInfo.GetMethods(BindingFlags.Static | BindingFlags.Public);
    var methodInfo = methods.First(e => e.Name == methodName);

    var parameterTypes = methodInfo.GetParameters().Select(e => e.ParameterType).ToArray();
    var parameters = args.Select((arg, i) =>
    {
        var converter = TypeDescriptor.GetConverter(parameterTypes[i]);
        return converter.ConvertFrom(arg);
    }).ToArray();

    return methodInfo.Invoke(null, parameters);
}

これで型名もメソッド名も引数も文字列で指定して呼び出すことが可能になりました。

補足

参照先のアセンブリが必要になる場合(Unity)

実行中のアセンブリの型だけじゃなくて参照先のアセンブリの型が必要になることなんてあるの?という疑問がある人もいると思うので補足です。
エディタ拡張でランタイムで使用する型を取得しようと思うとAssembly-CSharp-EditorAssembly-CSharpでアセンブリが別れているので必要になります。
実際これに気づかずに時間を無駄にしたことがあります。

まとめ

型名もメソッド名も引数も文字列で指定して呼び出すために必要なもの

  • すべての型の型情報(参照先のアセンブリも含む)
  • 型情報からメソッドの情報(MethodInfo)を取得する
  • メソッドの情報から引数の型情報を取得する
  • 文字列から引数の型に変換する
  • MethodInfo.Invoke

こういう記事を書くと複雑に思えますが知っていれば割とすんなり書けます。


明日は@nissy_gpさんの「TikTok APIを用いて広告レポートを自動的に取得する」です。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0