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);
    }
}

これを使ってみると

f:id:deyu_note:20160428160024p:plain

となり無事置き換えることができました。

その②メソッドチェーンをさかのぼって置き換える

ここからが本題です。今回は

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を作っています。

これを使ってみると

f:id:deyu_note:20160428161557p:plain

となり、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するとちゃんとさかのぼって置き換えられます。

f:id:deyu_note:20160428162454p:plain

Why?

ではなぜbase.VisitMethodCallを通すとうまくいくのでしょうか?

Reference Sourceを見てみましょう。

http://referencesource.microsoft.com/#System.Core/Microsoft/Scripting/Ast/ExpressionVisitor.cs,1cc1c82c4d2a41e2

Reference SourceによるとVisitMethodCallnode.ObjectVisitしています。 今回のコードで言えば、node.Method.Name == "Insert"のとき、node.Objects.Replace("hoge", "fuga").ToUpper()を表しているので、もしbase.VisitMethodCallを呼ばなければExpressionの走査がそこで終わってしまうことが分かります。というわけでbase.VisitMethodCallを呼び出す必要がありました。

今回のようなメソッドの置き換えに限らず、再帰的に複数個所Expressionを置き換えたい場合は、baseメソッドを呼び出す必要があります。(用途としてはLinqをいじりたいときぐらいかな...)