この記事は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);
ちなみにFullName
とName
の違いはいくつかありますが今回は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なども含めてすべて返しますが、今回必要なのは型の情報だけなのでSelect
でParamaterType
(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-Editor
とAssembly-CSharp
でアセンブリが別れているので必要になります。
実際これに気づかずに時間を無駄にしたことがあります。
まとめ
型名もメソッド名も引数も文字列で指定して呼び出すために必要なもの
- すべての型の型情報(参照先のアセンブリも含む)
- 型情報からメソッドの情報(
MethodInfo
)を取得する - メソッドの情報から引数の型情報を取得する
- 文字列から引数の型に変換する
MethodInfo.Invoke
こういう記事を書くと複雑に思えますが知っていれば割とすんなり書けます。
明日は@nissy_gpさんの「TikTok APIを用いて広告レポートを自動的に取得する」です。