演算子に関するセマンティクスが保存される Generic 型生成 ― Expression Tree による実行時コード生成 ―

「菊池 Blog」より.

たとえば、

class X<T> { bool IsEquals( T x,T y ) { return x==y; } }

==はobjectにも定義されているわけで、どのようなTに対しても本来は使用できます。

しかし上記のコードはコンパイルエラーになります。

op_Equalsがあるか無いかによってコードが(ILレベルで)変わってしまう(変えなければならない)から受け入れないのです。

このように、C#のGenericsにおいて実行コードには全く動的要素がありません。

動的に生成されるのは単なる型情報に過ぎません。型情報のみが動的であるわけです。

C++ の template はソースコードレベルのポータビリティですが,.NET の generics はアセンブリレベルのポータビリティを持ちます.すなわち template は A + B といったソースコードのセマンティクスを可搬にしますが,generics は演算子のオーバーロードや数値型ごとの加算 OpCode の違いを解決後の,IL レベルでの可搬性が求められることになる,というわけですね.
.NET の generics 型と演算子のオーバーロードの相性が悪い,という話が繰り返し出てくる背景には,比較対象である template と可搬性が実現される段階が違うためかな,と思います.



さて,ソースコードのセマンティクスの評価を実行時まで遅延させたいという話,最近どこかで見ましたよね?
波村さんのところより.

もちろん、Lambda 式と使った時上のコードは同一の Expression Tree が作られます。

ではなぜこの様な Data Structure が必要かというと、C# のコードがILに直接にコンパイルされた場合、言語の中のセマンティックスが失われてしまいます。 例えば上の例の中にあるLess Than オペレータは簡単な比較とジャンプのIL のオプコードを使って表現されます。Expression Tree をつかうことによって、直接 Less Than の Node をコードから使えるようになります。 もし Expression Tree がなかった場合、DLINQ などの機能を実現するには、IL を IL Stream を使って取り出し Semantic Analysis を IL に対してしなくてはならなくなってしまいます。

実際のところ今回行うような単なる実行時コード生成と,Expression Tree の真の意図にはズレがあるわけですが,折角なので Expression Tree を使った実行時コード生成を使用して,加算のセマンティクスを保った Generic 型を作ってみましょう.



.NET で動的に実行コードを生成する方法はいくつかあり,またその方法は増えつつあります.

  • (.NET 1.0 以降)CodeDOM やコンパイラによる動的コンパイル
  • (.NET 2.0 以降)Lightweight Code Generation (LCG)
  • (.NET 3.5 以降?)Expression Tree による動的コンパイル

テスト環境は以下のように結構怪しげなので,ご注意下さい.

  • Vista - July CTP (5472.5)
  • Visual Studio 2005 Team Edition for Software Developers
  • LINQ CTP May 2006

次のコードは,型 T が op_Addition という静的メソッドを持つ場合はそれを呼出し,そうでない場合は数値と思って Expression Tree による加算表現を使用するというラッパー構造体 Number<> です.
Number<> は加法演算子がオーバーロードされています.
生の型 T ではなく,Number とラップした型を使用することで,静的に生成されるコードは一意に定まるというわけです.型の定義上問題があれば実行時にエラーが発生します.

using System;
using System.Collections.Generic;
using System.Query;
using System.Expressions;
using System.Reflection;
using System.Drawing;

public struct Number<T>
    where T : struct
{
    private readonly T _value;
    public T Value
    {
        get
        {
            return _value;
        }
    }
    public Number(T value)
    {
        _value = value;
    }
    public static readonly Func<Number<T>, Number<T>, T> Add;
    static Number()
    {
        MethodInfo mi = typeof(T).GetMethod("op_Addition", System.Reflection.BindingFlags.Static | BindingFlags.Public );
        if( mi != null )
        {
            ParameterExpression p1 = Expression.Parameter(typeof(Number<T>), "a");
            ParameterExpression p2 = Expression.Parameter(typeof(Number<T>), "b");
            Expression body = Expression.Call(
               mi, null,
               new Expression[]{ 
               Expression.Property(p1, typeof(Number<T>).GetProperty("Value")),
               Expression.Property(p2, typeof(Number<T>).GetProperty("Value"))}
            );
            LambdaExpression exp = QueryExpression.Lambda(body, p1, p2);
            Add = exp.Compile() as Func<Number<T>, Number<T>, T>;
        }
        else
        {
            ParameterExpression p1 = Expression.Parameter(typeof(Number<T>), "a");
            ParameterExpression p2 = Expression.Parameter(typeof(Number<T>), "b");
            Expression body = Expression.Add(
               Expression.Property(p1, typeof(Number<T>).GetProperty("Value")),
               Expression.Property(p2, typeof(Number<T>).GetProperty("Value"))
            );
            LambdaExpression exp = QueryExpression.Lambda(body, p1, p2);
            Add = exp.Compile() as Func<Number<T>, Number<T>, T>;
        }
    }
    public static Number<T> operator + (Number<T> a, Number<T> b)
    {
        return new Number<T>( Add(a, b) );
    }
    public static implicit operator Number<T>(T a)
    {
        return new Number<T>(a);
    }
    public static implicit operator T (Number<T> a)
    {
        return a.Value;
    }
    public override string ToString()
    {
        return _value.ToString();
    }
}

なお Expression Tree の真骨頂は,今回のようにちまちま簡約して演算結果を返すのではなく,引数と Lambda 式そのものを返し,合成された Lambda 式をより上位のレイヤーで評価・変換するところだと思いますので,その辺は誤解無きよう.
コンパイルなんていつでもできるので,op_Addition というメソッドを公開するよりは,むしろ加算を行う Lambda 式自体を返た方が LINQ 的にはおもしろいと.



さてこの Number<> 型を利用して作ってみた複素数クラスがこちらです.

public struct Complex<T> where T:struct
{
    public readonly Number<T> Real;
    public readonly Number<T> Imaginal;
    public Complex(T r, T i)
    {
        this.Real = r;
        this.Imaginal = i;
    }
    public static Complex<T> operator +(Complex<T> a, Complex<T> b)
    {
        return new Complex<T>( a.Real + b.Real, a.Imaginal + b.Imaginal );
    }
    public override string ToString()
    {
        return string.Format("{0}+{1}i", Real, Imaginal);
    }
}

最後に利用サンプルです.

static class Program
{
    static void Main(string[] args)
    {
        Number<float> a = 1.23f;
        Number<float> b = 2.34f;
        var c = a + b;
        c += 3.46f;

        var f = new Complex<double>( 1.0f, 2.0f );
        var g = new Complex<double>( 0.0f, 0.5f );

        var h = f + g;

        Console.WriteLine( c );
        Console.WriteLine( h );

        var x = new Complex<Size>( new Size(2, 3), new Size(1,2) );
        var y = new Complex<Size>( new Size(-1, 2), new Size(3,0) );

        var z = x + y;
        Console.WriteLine( z );
    }
}

実行結果はこんな感じ.

7.03
1+2.5i
{Width=1, Height=5}+{Width=4, Height=2}i