ノートの端の書き残し

UnityやらC#やら触っていたときのメモ

【C#】【リフレクション】任意の型のインスタンスを文字列から生成する

MyClassというクラスがあったとします。
普通、MyClassインスタンスを生成したければこうです。

MyClass instance = new MyClass();

さて、C#は型がガッチリ決まっているのが素晴らしいところだとは思うのですが、それが困るケースもやはりあります。
他の言語で作ったJsonからデシリアライズしたいとか、実行時に生成したい型が初めてわかる、みたいなことはよくありますね。 作りたいインスタンスの型とそこに入れるJsonを両方文字列で指定できたらとても便利です。 そういった動的に変化する型に対応する際に使えるのがリフレクションです。

文字列からインスタンスを生成する

MyClassを文字列から生成したければこのように記述します。

object instance = System.Activate.CreateInstance(Type.GetType("MyClass"));

引数が文字列ですので、例えばテキストを読み込んで生成すれば色んな型がこれ一つで生成できてしまいます。
が、これは万能ではありません。 そもそもActivate.CreateInstanceというのはnewと書き方が違うだけでやっていることは同じです。 つまり上の記述は実質以下と同じ。

object instance = new MyClass();

ここでもしMyClassに引数付きのコンストラクタが宣言されていればどうでしょうか。
その場合引数無しのコンストラクタは自分で宣言していなければMyClassにはありませんね。 なのに無理やりリフレクションでnew MyClass()を呼んでしまうと、
MissingMethodExceptionという例外を吐いてしまいます。
これは読んでその通り、そんなメソッドは存在しないという例外です。 ありもしない引数無しコンストラクタを無理やり呼んでしまったようです。

引数無しのコンストラクタが無いクラスの場合

例えばMyClassがint型1つを引数に取るコンストラクタを持っているならば、

object instance = System.Activate.CreateInstance(Type.GetType("MyClass"), 1);

と書けばnew MyClass(1)と同じです。今回は問題なくインスタンスを生成できます。
が、これはMyClassを知っている場合だからできること。実行時に初めて型がわかるような場合に使いたいのがリフレクションなので、コンストラクタの引数もこんな形で指定できるはずがありません。

コンストラクタの引数を知る

ここからはusing System.Reflection;を記述してください。 MyClassに設定されたコンストラクタを知りたい場合。

ConstructorInfo[] constructors = Type.GetType("MyClass").GetConstructors();

が使えます。
これはConstructorInfoの配列を返すのでどれを使えばいいのか悩みますが、今回はインスタンス生成後にJsonやらなんやらを用いて値を代入していくので、どのコンストラクタを使おうが関係ありません。 とりあえずIndex = 0のやつを使いましょうか。
なお、コンストラクタを実行したいときはInvokeです。
引数にはとりあえずobjectの配列を渡しましょう。

object instance = constructors[0].Invoke(object[]);

渡すときの変数はobjectの配列にしますが、中身は何でもいいわけではないです。
intの引数が欲しいのに実体がstringなobjectを渡してしまったら実行時エラーですよね。
ではコンストラクタの引数はどうやって知るのかというと。

ParameterInfo[] parameters = constructors[0].GetParameters();

ParameterInfoは引数に関して色々な情報を持っていますが、今知りたいのはその型です。

Type[] arguTypes = parameters.Select(paramInfo => paramInfo.ParameterType).ToArray();

ParameterTypeがそれです。今回は引数の型一覧が欲しいのでLinqでも使いましょうか。

さあこれで、インスタンス生成の準備は整いました。

あらゆるクラスのインスタンスを文字列から生成する

以上をまとめると

  • インスタンス生成にはActivate.CreateInstance(Type)を用いる
  • 引数付きのコンストラクタを使う場合はConstructorInfo.Invoke(object[])

クラスの中にもクラスがあるのが普通ですから再帰的にする必要もあるでしょう。
以上のことを踏まえてこんなメソッドが書けました。

/// <summary>
/// typeのインスタンスを生成する
/// </summary>
/// <param name="type">生成したいインスタンスの型</param>
/// <returns></returns>
private static object createInstance(Type type)
{
    object obj;
    try {
        obj = Activator.CreateInstance(type);
        // 引数無しコンストラクタが無い場合は引数情報を引っ張ってきてConstructorInfoから生成する
    } catch(MissingMethodException) {
        var constructor = type.GetConstructors()[0];
        var parameters = constructor.GetParameters().Select(parInfo => createInstance(parInfo.ParameterType)).ToArray();
        obj = constructor.Invoke(parameters);
    }
    return obj;
} 

なかなかコンパクトに収まりましたね。

追記

上記のままだと特定のケースでエラーを吐いてしまいます(Char*にコンストラクタが無く怒られるっぽい…)。 コンストラクタが無いのは流石にどうにもできない……

対応としては、catchの中にさらにtry-catchを入れて、

try
{
    var constructor = type.GetConstructors()[0];
    var parameters = constructor.GetParameters().Select(parInfo => createInstance(parInfo.ParameterType)).ToArray();
    obj = constructor.Invoke(parameters);
}
    catch
{
    obj = null;
}

とすればChar*については問題なさそうですが、若干怪しい気もするので、defaultで切り抜けた方がいいかも。

public static object CreateInstance(Type type)
{
    var _createInstance = ((Func<int>)CreateInstance<int>).Method.GetGenericMethodDefinition();
    return _createInstance.MakeGenericMethod(type).Invoke(null, null);
}

public static T CreateInstance<T>()
{
    T obj = default;
    try
    {
        obj = Activator.CreateInstance<T>();
    }
    catch
    {
    try
        {
            var constructor = typeof(T).GetConstructors().[0];
            var parameters = constructor.GetParameters().Select(parInfo => CreateInstance(parInfo.ParameterType)).ToArray();
            obj = (T)constructor.Invoke(parameters);
        }
        catch { }
    }
    return obj;
}