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

デコンパイルされたコードを見てみると

  • ラムダ式で初期化された方はコンパイラがキャッシュするように特別なコードを埋め込んでいる
  • メソッドグループで初期化された方はほぼそのまま

ということがわかります。

まとめ

以上のことから複数回同じ形のFunc<,>を使う場合はラムダ式で初期化した方がよさそうです。

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