Unityで使えるC#のバージョンが(だいぶ前に)上がったのでExpression TreeをUnityで使ってみます。
- iOSのIL2CPPビルドではまだ使えない
- Expression Tree?
- とりあえず書いてみる
- Reflectionの代わりに使って変数に代入する
- Expression Treeを使ってメソッドを呼ぶ
- dynamicについて
- 関連
- 参考
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/
ただしエディタ拡張などではもちろん使えます。
Expression Tree?
Expression Treeの基本的なところは以下の記事にまとめました。
この機能を使うと動的にラムダ式をコンパイルできます。
そしてReflectionの代わりに使うとかなり早くなるようです。
また、Expression TreeはC#のバージョンアップに伴い色々と機能が拡充されたようで、
Unityでは.Netのバージョンを4系にするといい感じに使えます。
とりあえず書いてみる
とりあえず書いてみるには下記の記事がとても分かり易かったです。
上記をみながら簡単なソースコードを書いてみます。
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を使うとより簡単に書けそうです。
ただUnityで使おうとすると、API Compatibility Levelを.Net 4.xにしないといけませんでした。
今のところ.Net Standard 2.0が推奨されているようなので、
この設定でも使えるExpression Treeを使っておくのが無難そうです。
関連
参考
https://forum.unity.com/threads/are-c-expression-trees-or-ilgenerator-allowed-on-ios.489498/