【Unity】【C#】Reflectionを使ってメソッドを呼んだり変数を書き換えたりキャッシュして高速化したりする

C#でReflectionを使ってメソッドを呼んだり変数を書き換えたりする方法です。

メソッドを呼ぶ

まず引数のないpublicなメソッドを呼んでみます。

using UnityEngine;

public class Example : MonoBehaviour
{
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.A)) {
            // このクラスの「Test」という名前のメソッドを取得する
            var setMethod = this.GetType().GetMethod("Test");
            if (setMethod != null) {
                // 実行
                setMethod.Invoke(this, null);
            }
        }
    }

    public void Test()
    {
        Debug.Log("Test");
    }
}

privateやprotectedなメソッドを呼ぶ

前節の実装ではprivateなメソッドは呼べません。
privateやprotectedなメソッドを呼ぶには次のようにします。

using System.Reflection;
using UnityEngine;

public class Example : MonoBehaviour
{
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.A)) {
            // privateやprotectedなものも取得
            // Instanceも指定する必要があるので注意
            var setMethod = this.GetType().GetMethod("Test", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
            if (setMethod != null) {
                // 実行
                setMethod.Invoke(this, null);
            }
        }
    }

    private void Test()
    {
        Debug.Log("Test");
    }
}

オーバーロード対応

さてここでTest()のほかに引数付きオーバーロードTest(int)が定義されたとします。

public void Test()
{
    Debug.Log("Test");
}

public void Test(int a)
{
    Debug.Log("Test : " + a);
}

前節までの実装でこれを呼ぼうとすると次のような例外が発生します。

AmbiguousMatchException: Ambiguous match found.

これはどちらのメソッドを取得すればいいのかわからないためです。
引数の型と数を明示的に指定するには次のように書きます。

using System;
using System.Reflection;
using UnityEngine;

public class Example : MonoBehaviour
{
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.A)) {
            var setMethod = this.GetType()
                .GetMethod
                (
                    "Test",
                    BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
                    null,
                    new Type[0], // ここに引数の型を配列で指定
                    null
                );
            if (setMethod != null) {
                setMethod.Invoke(this, null);
            }
        }
    }

    public void Test()
    {
        Debug.Log("Test");
    }

    public void Test(int a)
    {
        Debug.Log("Test : " + a);
    }
}

引数が存在する方のTestを呼びたいときには次のように書きます。

using UnityEngine;

public class Example : MonoBehaviour
{
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.A)) {
            var setMethod = this.GetType()
                .GetMethod
                (
                    "Test",
                    new [] { typeof(int) } // ここに引数の型を配列で指定
                );
            if (setMethod != null) {
                setMethod.Invoke(this, new object[]{ 1 }); // 引数を指定して実行
            }
        }
    }

    public void Test()
    {
        Debug.Log("Test");
    }

    public void Test(int a)
    {
        Debug.Log("Test : " + a);
    }
}

メソッドを呼ぶときのオススメの書き方

上記を踏まえて、メソッドを呼ぶときには次のように書いておくといいです。

var setMethod = this.GetType()
    .GetMethod
    (
        "Test",
        BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
        null,
        new [] { typeof(int) },
        null
    );
if (setMethod != null) {
    setMethod.Invoke(this, new object[]{ 1 }); // 引数を指定して実行
}

変数を書き換える

次にプロパティやフィールドを書き換えてみます。
要領は同じです。

using System.Reflection;
using UnityEngine;

public class Example : MonoBehaviour
{
    public int TestProperty { get; set; }

    public int testField;

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.A)) {
            // プロパティ
            var setMethod = this.GetType()
                .GetProperty("TestProperty", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
                .GetSetMethod(); // 代入用メソッドを取得
            if (setMethod != null) {
                setMethod.Invoke(this, new object[]{ 1 }); // 引数を指定して実行
            }
        }

        if (Input.GetKeyDown(KeyCode.B)) {
            // フィールド
            var fieldInfo = this.GetType()
                .GetField("testField", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
            if (fieldInfo != null) {
                fieldInfo.SetValue(this, 1);
            }
        }
    }
}

例外対応

Reflectionでメソッドや変数を取得する際には例外が発生する可能性があります。
例えばパラメータ数が間違えていた場合には次のような例外が発生します。

TargetParameterCountException: Number of parameters specified does not match the expected number.

このあたりが発生し得る場合にはきちんと例外処理しておいたほうがいいです。

if (Input.GetKeyDown(KeyCode.A)) {
    MethodInfo info = null;
    try {
        info = this.GetType().GetMethod("Test");
    }
    catch (System.Exception) {
        // 取得失敗時の処理
    }
    if (info != null) {
        info.Invoke(this, new object[]{ 1 });
    }
}

パフォーマンスのためにキャッシュ

Reflectionによるメソッドや変数の取得は重いです。
重いのでキャッシュをする必要があります。

キャッシュをするにはDelegate.CreateDelegateを使います。

using System;
using System.Reflection;
using UnityEngine;

public class Example : MonoBehaviour
{
    private Action<int> _delegate;

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.A)) {
            Execute();
        }
    }

    private void Execute()
    {
        if (_delegate == null) {

            MethodInfo info = null;
            try {
                info = this.GetType()
                    .GetMethod
                    (
                        "Test",
                        BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
                        null,
                        new [] { typeof(int) },
                        null
                    );
            }
            catch (Exception e) {
                // 取得失敗時の処理
                throw e;
            }
            if (info != null) {
                // デリゲートを生成
                _delegate =(Action<int>) Delegate.CreateDelegate(typeof(Action<int>), this, info);
            }
        }

        _delegate?.Invoke(2);
    }

    public void Test()
    {
        Debug.Log("Test");
    }

    public void Test(int a)
    {
        Debug.Log("Test : " + a);
    }
}

参考

neue.cc