【Unity】【C#】Expression TreeをReflectionの代わりに使う

Unityで使えるC#のバージョンが(だいぶ前に)上がったのでExpression TreeをUnityで使ってみます。

Unity2018.2
Scripting Runtime Version : .Net 4.x
API Compatibility Level : .Net Standard 2.0

iOSのIL2CPPビルドではまだ使えない

Expression TreeはiOS + IL2CPPでビルドするとランタイムエラーになります。
本記事の執筆時点ではまだ一部のメソッドしか使えないようなので、
モバイルでまともに使えるようになるのはもっと先になりそうです。

https://forum.unity.com/threads/are-c-expression-trees-or-ilgenerator-allowed-on-ios.489498/

docs.unity3d.com

www.wintellect.com

ただしエディタ拡張などではもちろん使えます。

Expression Tree?

Expression Treeの基本的なところは以下の記事にまとめました。

light11.hatenadiary.com

この機能を使うと動的にラムダ式コンパイルできます。
そしてReflectionの代わりに使うとかなり早くなるようです。

ds-optim.hateblo.jp

また、Expression TreeはC#のバージョンアップに伴い色々と機能が拡充されたようで、
Unityでは.Netのバージョンを4系にするといい感じに使えます。

とりあえず書いてみる

とりあえず書いてみるには下記の記事がとても分かり易かったです。

qiita.com

上記をみながら簡単なソースコードを書いてみます。

using UnityEngine;
using System;
using System.Linq.Expressions;

public class Example : MonoBehaviour
{
    private void Start()
    {
        // ラムダ式の引数として渡す型のExpression.Parameterを定義しておく
        var x = Expression.Parameter(typeof(int));
        
        // ラムダ式を作るための式木
        var lambda = 
            Expression.Lambda<Func<int, int>>(
                // 加算
                Expression.Add(
                    x,
                    Expression.Constant(5) // 定数
                ),
                // ラムダ式の引数
                x
            );
        
        // コードをコンパイル > funcにラムダ式が入ってくる
        var func = lambda.Compile();
        // 実行
        var output = func.Invoke(12);
        // 17
        Debug.Log(output);
    }
}

とりあえず足し算ができました。

Reflectionの代わりに使って変数に代入する

Reflectionを使って変数に値を代入するにはこんな感じでやると思います。

GetType().GetField("fieldName").SetValue(target, value);

これをExpression Treeで書いてみます。

using UnityEngine;
using System.Linq.Expressions;
using System;

public class Example : MonoBehaviour
{

    [SerializeField]
    private int _example;

    private Action<Example, int> _cachedMethod;

    private int _currentInt = 0;

    private void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            if (_cachedMethod == null)
            {
                // コンパイルは重いのでキャッシュしておく
                _cachedMethod = CreateAssignPropertyMethod<Example, int>("_example");
            }
            _currentInt += 5;
            _cachedMethod.Invoke(this, _currentInt);
        }
    }

    /// <summary>
    /// 一つ目の引数に与えたオブジェクトのプロパティに二つ目の引数の値を代入するActionを作成する
    /// </summary>
    public static Action<TTarget, TValue> CreateAssignPropertyMethod<TTarget, TValue>(string propertyOrFieldName)
    {
        // ラムダ式の引数をExpression.Parameterにする
        var targetParam = Expression.Parameter(typeof(TTarget));
        var valueParam = Expression.Parameter(typeof(TValue));

        var lambda =
            Expression.Lambda<Action<TTarget, TValue>>(
                // ラムダ式で処理する内容
                Expression.Assign(
                    Expression.PropertyOrField(
                        targetParam,
                        propertyOrFieldName
                    ),
                    valueParam
                )
                // ラムダ式の引数
                , targetParam
                , valueParam
            );

        return lambda.Compile();
    }
}

説明はコメントに書いたとおりです。
Compile()メソッドは重いようなので、キャッシュするのがポイントです。

ちなみにジェネリックを使わないで書くと次のようになります。
こちらの方が実際には使い勝手はいいかも。

using UnityEngine;
using System.Linq.Expressions;
using System;

public class Example : MonoBehaviour
{

    [SerializeField]
    private int _example;

    private Action<object, object> _cachedMethod;

    private int _currentInt = 0;

    private void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            if (_cachedMethod == null)
            {
                // コンパイルは重いのでキャッシュしておく
                _cachedMethod = CreateAssignPropertyMethod(this.GetType(), typeof(int), "_example");
            }
            _currentInt += 5;
            _cachedMethod.Invoke(this, _currentInt);
        }
    }

    /// <summary>
    /// 一つ目の引数に与えたオブジェクトのプロパティに二つ目の引数の値を代入するActionを作成する
    /// </summary>
    public static Action<object, object> CreateAssignPropertyMethod(Type targetType, Type propertyOrFieldType, string propertyOrFieldName)
    {
        // ラムダ式の引数はobject型
        var targetParam = Expression.Parameter(typeof(object));
        var valueParam = Expression.Parameter(typeof(object));

        var lambda =
            Expression.Lambda<Action<object, object>>(
                Expression.Assign(
                    Expression.PropertyOrField(
                        // プロパティを取得する前にキャストが必要
                        Expression.Convert(targetParam, targetType),
                        propertyOrFieldName
                    ),
                    // こちらもキャストが必要
                    Expression.Convert(valueParam, propertyOrFieldType)
                )
                // ラムダ式の引数
                , targetParam
                , valueParam
            );

        return lambda.Compile();
    }
}

Expression Treeを使ってメソッドを呼ぶ

次にExpression Treeを使ってメソッドを呼んでみます。

using UnityEngine;
using System.Linq.Expressions;
using System;
using System.Reflection;

public class Example : MonoBehaviour
{
    private Action<Example> _cachedMethod;
    
    private void ExampleMethod()
    {
        Debug.Log("called.");
    }

    private void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            if (_cachedMethod == null)
            {
                // コンパイルは重いのでキャッシュしておく
                _cachedMethod = CreateMethodCallingMethod<Example>("ExampleMethod", BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic);
            }
            _cachedMethod.Invoke(this);
        }
    }

    public static Action<TTarget> CreateMethodCallingMethod<TTarget>(string methodName, BindingFlags bindingFlags)
    {
        var targetParam = Expression.Parameter(typeof(TTarget));
        
        var methodInfo = typeof(TTarget).GetMethod(methodName, bindingFlags);
        
        var lambda =
            Expression.Lambda<Action<TTarget>>(
                // メソッドを呼ぶExpression
                Expression.Call(
                    targetParam,
                    methodInfo
                )
                // ラムダ式の引数
                , targetParam
            );

        return lambda.Compile();
    }
}

ジェネリックを使わないとこんな感じです。

using UnityEngine;
using System.Linq.Expressions;
using System;
using System.Reflection;

public class Example : MonoBehaviour
{
    private Action<Example> _cachedMethod;
    
    private void ExampleMethod()
    {
        Debug.Log("called.");
    }

    private void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            if (_cachedMethod == null)
            {
                // コンパイルは重いのでキャッシュしておく
                _cachedMethod = CreateMethodCallingMethod(typeof(Example), "ExampleMethod", BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic);
            }
            _cachedMethod.Invoke(this);
        }
    }

    public static Action<object> CreateMethodCallingMethod(Type classType, string methodName, BindingFlags bindingFlags)
    {
        var targetParam = Expression.Parameter(typeof(object));
        
        var methodInfo = classType.GetMethod(methodName, bindingFlags);
        
        var lambda =
            Expression.Lambda<Action<object>>(
                // メソッドを呼ぶExpression
                Expression.Call(
                    Expression.Convert(targetParam, classType),
                    methodInfo
                )
                // ラムダ式の引数
                , targetParam
            );

        return lambda.Compile();
    }
}

dynamicについて

この辺りはdynamicを使うとより簡単に書けそうです。

ufcpp.net

ただUnityで使おうとすると、API Compatibility Levelを.Net 4.xにしないといけませんでした。
今のところ.Net Standard 2.0が推奨されているようなので、
この設定でも使えるExpression Treeを使っておくのが無難そうです。

関連

light11.hatenadiary.com

参考

https://forum.unity.com/threads/are-c-expression-trees-or-ilgenerator-allowed-on-ios.489498/

docs.unity3d.com

www.wintellect.com

ds-optim.hateblo.jp

qiita.com