演算子に関するセマンティクスが保存される 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 と可搬性が実現される段階が違うためかな,と思います.
- id:ladybug:20040812
- id:akiramei:20040820#p2
- Generics で四則演算がしたい。
- 演算にジェネリックが含まれる場合の対処方法
さて,ソースコードのセマンティクスの評価を実行時まで遅延させたいという話,最近どこかで見ましたよね?
波村さんのところより.
もちろん、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