Funcの初期化の違いについて
Func(T, TResult) デリゲート (System)である変数の初期化について一般的に下記の二種類あると思います。
//初期化方法1:ラムダ式 Func<int, int> f1 = i => Square(i); //初期化方法2:メソッドグループ Func<int, int> f2 = Square; //ちなみにSquareは public static int Square(int i) { return i*i; }
デコンパイルして違いを見てみる
using System; namespace ConsoleApplication1 { class Program { static void Main(string[] args) { Func<int, int> f1 = i => Square(i); Func<int, int> f2 = Square; } public static int Square(int i) { return i*i; } } }
このコードを対象にデコンパイルして2種類の初期化の違いを見てみます。 結果このようなコードが吐き出されました。
using System; using System.Runtime.CompilerServices; namespace ConsoleApplication1 { internal class Program { [CompilerGenerated] [Serializable] private sealed class <>c { public static readonly Program.<>c <>9 = new Program.<>c(); public static Func<int, int> <>9__0_0; internal int <Main>b__0_0(int i) { return Program.Square(i); } } private static void Main(string[] args) { Func<int, int> arg_20_0; if ((arg_20_0 = Program.<>c.<>9__0_0) == null) { Program.<>c.<>9__0_0 = new Func<int, int>(Program.<>c.<>9.<Main>b__0_0); } Func<int, int> f2 = new Func<int, int>(Program.Square); } public static int Square(int i) { return i * i; } } }
デコンパイルされたコードを見てみると
ということがわかります。
まとめ
ExpressionVisitorでExpressionTreeをいじる
ExpressionVisitorを使うと、ExpressionTreeを走査したり一部置き換えたりすることができます。 ここではExpressionVisitorを使ってメソッドチェーンの一部を置き換えてみようと思います。
その①メソッドを一つだけ置き換える
string s => s.Replace("hoge", "fuga").ToUpper().Insert(0, "piyo");
に相当するExpressionTreeを
string s => s.Replace("hoge", "fuga").ToUpper().Insert(0, "foo"); //"piyo" -> "foo"
に置き換えます。
ExpressionVisitor実装
class MyVisior : ExpressionVisitor { protected override Expression VisitMethodCall(MethodCallExpression node) { if (node.Method.Name == "Insert") { var index = Expression.Constant(0); var value = Expression.Constant("foo"); return Expression.Call(node.Object, node.Method, index, value); } return base.VisitMethodCall(node); } }
これを使ってみると
となり無事置き換えることができました。
その②メソッドチェーンをさかのぼって置き換える
ここからが本題です。今回は
string s => s.Replace("hoge", "fuga").ToUpper().Insert(0, "piyo");
に相当するExpressionTreeを
string s => s.Substring(4).ToUpper().Insert(0, "foo"); //"piyo" -> "foo"
に置き換えます。
ExpressionVisitor実装
class MyVisior2 : ExpressionVisitor { protected override Expression VisitMethodCall(MethodCallExpression node) { if (node.Method.Name == "Insert") { var index = Expression.Constant(0); var value = Expression.Constant("foo"); return Expression.Call(node.Object, node.Method, index, value); } if (node.Method.Name == "Replace") { var startIndex = Expression.Constant(4); var method = typeof(string).GetMethod("Substring", new[] {typeof(int)}); return Expression.Call(node.Object, method, startIndex); } return base.VisitMethodCall(node); } }
その①と同じようにReplaceのときに新しくExpressionを作っています。
これを使ってみると
となり、Replaceが置き換えられていません。
解決策
class MyVisior3 : ExpressionVisitor { protected override Expression VisitMethodCall(MethodCallExpression node) { if (node.Method.Name == "Insert") { var index = Expression.Constant(0); var value = Expression.Constant("foo"); //returnではなくbase.VisitMethodCallへ渡るようにする //return Expression.Call(node.Object, node.Method, index, value); node = Expression.Call(node.Object, node.Method, index, value); } if (node.Method.Name == "Replace") { var startIndex = Expression.Constant(4); var method = typeof(string).GetMethod("Substring", new[] {typeof(int)}); //returnではなくbase.VisitMethodCallへ渡るようにする //return Expression.Call(node.Object, method, startIndex); node = Expression.Call(node.Object, method, startIndex); } return base.VisitMethodCall(node); } }
その①②では新しく作ったExpressionをそのままreturnしていましたが、上記のようにbase.VisitMethodCall
に渡してからreturnするとちゃんとさかのぼって置き換えられます。
Why?
ではなぜbase.VisitMethodCall
を通すとうまくいくのでしょうか?
Reference Sourceを見てみましょう。
Reference SourceによるとVisitMethodCall
はnode.Object
をVisit
しています。
今回のコードで言えば、node.Method.Name == "Insert"
のとき、node.Object
はs.Replace("hoge", "fuga").ToUpper()
を表しているので、もしbase.VisitMethodCall
を呼ばなければExpressionの走査がそこで終わってしまうことが分かります。というわけでbase.VisitMethodCall
を呼び出す必要がありました。
今回のようなメソッドの置き換えに限らず、再帰的に複数個所Expressionを置き換えたい場合は、baseメソッドを呼び出す必要があります。(用途としてはLinqをいじりたいときぐらいかな...)