式木とは
C#の構文を動的に生成できる機能です。
メソッドでの生成とラムダ式での生成がサポートされています。
Expression<Func<int,int,int>>exp1=(a,b)=>a+b;varparamA=Expression.Parameter(typeof(int),"a");varparamB=Expression.Parameter(typeof(int),"b");Expression<Func<int,int,int>>exp2=Expression.Lambda<Func<int,int,int>>(Expression.Add(paramA,paramB),paramA,paramB);// exp1とexp2は同等varf1=exp1.Compile();varf2=exp2.Compile();Console.WriteLine(f1(3,4));// -> 7Console.WriteLine(f2(3,4));// -> 7
式木から別の式木を作ってみる
T
型の引数からIComparable<K>
であるK
を返すようなラムダ式を元にIComparer<T>
を下記のように作れます。
例えば、コンストラクタに(string s) => -s.Length
を渡すと文字列の長さの降順となるような結果が得られます。
publicclassExpComparer<T,K>:IComparer<T>whereK:IComparable<K>{privateclassParameterReplaceVisitor:ExpressionVisitor{privatereadonlyParameterExpressionfrom;privatereadonlyParameterExpressionto;publicParameterReplaceVisitor(ParameterExpressionfrom,ParameterExpressionto){this.from=from;this.to=to;}protectedoverrideExpressionVisitParameter(ParameterExpressionnode)=>node==from?to:base.VisitParameter(node);}privatereadonlyComparison<T>func;publicExpComparer(Expression<Func<T,K>>expression){varparamA=expression.Parameters[0];varparamB=Expression.Parameter(typeof(T));varexp2=(Expression<Func<T,K>>)newParameterReplaceVisitor(paramA,paramB).Visit(expression);varcompExp=Expression.Lambda<Comparison<T>>(Expression.Call(expression.Body,typeof(K).GetMethod(nameof(IComparable<K>.CompareTo),new[]{typeof(K)}),exp2.Body),paramA,paramB);this.func=compExp.Compile();}publicintCompare(Tx,Ty)=>func(x,y);publicoverrideboolEquals(objectobj)=>obj!=null&&GetType()==obj.GetType();publicoverrideintGetHashCode()=>GetType().GetHashCode();}
以下、ポイントごとに解説します
paramA, paramB
Comparison<T>
は引数を2つ持つので、それに対応するparamA
とparamB
を用意します。
片方は元のParameterExpression
をそのまま流用でOKです。
ExpressionVisitor
引数の式木のパラメータを新たに生成したparamB
で置き換える役割です。
Expression.Lambda<Comparison<T>>
ここでCompareTo
の呼び出しを構築します。
expressionをp => -p
だったとき
// expressionを p => -pとする(p1,p2)=>-p1.CompareTo(-p2)
というようになります。
expression.Body
とexp2.Body
の順番を間違えると
// expressionを p => -pとする(p1,p2)=>-p2.CompareTo(-p1)
になるので注意
Compile
Compile
メソッドでLambdaExpressionをdelegateに変換します。
あとは普通のdelegateとして扱えます。
補足:ParameterExpressionについて
Expression<Func<string,int>>expression=a=>a.Length;varparamA=expression.Parameters[0];varparamB=Expression.Parameter(typeof(string),"a");varexp2=(Expression<Func<string,int>>)newParameterReplaceVisitor(paramA,paramB).Visit(expression);varcompExp=Expression.Lambda<Comparison<string>>(Expression.Call(expression.Body,typeof(int).GetMethod(nameof(IComparable<int>.CompareTo),new[]{typeof(int)}),exp2.Body),paramA,paramB);varfunc=compExp.Compile();Console.WriteLine(compExp);// -> (a,a) = a.Length.CompareTo(a.Length)
上記のようにexpression
のパラメータがa
となっている場合に、生成される式木が(a,a) = a.Length.CompareTo(a.Length)
となりますが問題ありません。
ParameterExpression
はName
プロパティが同一でもインスタンスが別(object.ReferenceEquals
での比較がfalse
)の場合は別の変数として扱われるためです。
逆にいうと、
Expression<Func<string,int>>expression=Expression.Lambda<Func<string,int>>(Expression.Property(Expression.Parameter(typeof(string),"a"),nameof(string.Length)),Expression.Parameter(typeof(string),"a"));
のような式木はa => a.Length
となりますが不正です。
Console.WriteLine(expression);// -> a => a.LengthConsole.WriteLine(expression.Compile()("f42"));// Unhandled exception. System.InvalidOperationException: variable 'a' of type 'System.String' referenced from scope '', but it is not defined
ラムダの引数のa
とa.Lengthのa
が別の変数として扱われるためです。