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をいじりたいときぐらいかな...)