Ver. 3.0
ラムダ式は、Expression 型の変数に代入すると、 匿名デリゲート(実行可能なコード)ではなく式木(式の意味を表す木構造データ)としてコンパイルされます。 例えば、以下の2つのコードは同じ意味になります。
Expression<Func<int, int>> e = x => x + 5
var e = Expression.Lambda<Func<int, int>>( Expression.Add( Expression.Parameter(typeof(int), "x"), Expression.Constant(5)), Expression.Parameter(typeof(int), "x"));
ここでは、 どういうラムダ式を書くと、どういう式木が得られるのかを簡単に説明していきます。
サンプルコード → TypesForTest.cs、 ExpressionTest.cs。
まず先に、式木を使う上での制約について。 ラムダ式ならば何でも式木にできるというわけではありません。
ラムダ式には、以下に例示するような2つの記法、 1文だけのタイプとブロックを持つタイプがあります。
Func<int, int> f = x => x + 5
Func<int, int> f = x => { int p = 1; for (int i = 0; i < x; ++i) p *= x; return p; }
前者は、だた1つだけの式からなっていて、 {} や return を省略できます。 後者は、{} ブロック内に複数の文を並べてかけます。
このうち、式木にできるのは前者(1文だけのラムダ式)だけです。
そうなると、結構強い制約がかかってきます。 例えば、for, while, switch などの制御構文や、x = 0 といったような代入式は式木にできません。 あと、インクリメント・デクリメントも、実質的には加減算+代入なので、式木にできません。 また、ラムダ式内でローカル変数を定義できません。
一方、C# 3.0 で導入されたオブジェクトイニシャライザ(参考: 「イニシャライザ」 )を使えば、結構複雑な式も書けたりします。 例えば以下のような感じ。
Expression<Func<LineSegment>> e = () =>
new LineSegment
{
Start = { X = 0, Y = 0 },
End = { X = 1, Y = 1 },
};
前節の例でちょこっと出てきた Expression.Lambda や Expression.Add メソッドによって生成されるのは、 LambdaExpression 型や BinaryExpression 型の変数になりますが、 これらは全て、Expression 型の派生クラスになります。
Expression 型の派生クラスは、直接 new することはできず、 Expression 型の static メソッド(Lambda や Add)を使って生成します。
Expression 型は NodeType というプロパティを持っていて、 例えば、加算なら NodeType == ExpressionType.Add になります。
生成用の static メソッド、 具体的な型、 NodeType がそれぞればらばらで、少し複雑なんですが、 いくつか先に例を示します。
表1: Expression 型の例
| 対応するコード | 生成メソッド | NodeType | 型 |
|---|---|---|---|
| + | Add | Add | BinaryExpression |
| new | New | New | NewExpression |
| () => 0 | Lambda<Func<int>> | Lambda | LambdaExpression<Func<int>> |
百聞は一見にしかずということで、 次節以降では、ラムダ式と式木の対応関係を実例を挙げて紹介していきます。 それに先立って、いくつか補助関数や変数を用意しておきます。
まず、Expression 型を作りやすくするために (型推論が働きやすくするために)、 以下のような補助関数を用意します。
static partial class Make { public static Expression<Func<TR>> Expression<TR>(Expression<Func<TR>> e) { return e; } public static Expression<Func<T1, TR>> Expression<T1, TR>(Expression<Func<T1, TR>> e) { return e; } public static Expression<Func<T1, T2, TR>> Expression<T1, T2, TR>(Expression<Func<T1, T2, TR>> e) { return e; } public static Expression<Func<T1, T2, T3, TR>> Expression<T1, T2, T3, TR>(Expression<Func<T1, T2, T3, TR>> e) { return e; } public static Expression<Func<T1, T2, T3, T4, TR>> Expression<T1, T2, T3, T4, TR>(Expression<Func<T1, T2, T3, T4, TR>> e) { return e; } }
また、(簡易的にではありますが、) 2つの式木が一致するかどうかを判定する関数を用意します。
/// <summary> /// 式木の構造が一致してれば、少なくとも ToString の結果は一致するので、 /// それで2つの式木の一致性を判定。 /// </summary> static void SimpleCheck(Expression e1, Expression e2) { if (e1.ToString() != e2.ToString()) { Console.Write("not match: {0}, {1}\n", e1, e2); } }
さらに、Expression.Parameter は頻繁に出てくるものなので、 あらかじめ Parameter を作って変数に代入しておきます。
static ParameterExpression intX = Expression.Parameter(typeof(int), "x"); static ParameterExpression intY = Expression.Parameter(typeof(int), "y"); static ParameterExpression boolX = Expression.Parameter(typeof(bool), "x"); static ParameterExpression boolY = Expression.Parameter(typeof(bool), "y");
それから、テスト用に、Point, LineSegment, Polyline などの型を定義します → TypesForTest.cs。
サンプル: ExpressionTest.cs 中の Lambda() メソッド。
ラムダ式そのものは LambdaExpression 型か、 Expression<T> ジェネリック型(LambdaExpression のサブクラス)になります。
Lambda メソッドに、ラムダ式の本体(Body)とパラメータリスト(Paramters)を渡して生成します。 ちなみに、パラメータと定数はそれぞれ、Parameter、Constant メソッドで生成します。
(以後、サンプルコード中では、 SimpleCheck メソッドの1つ目の引数と2つ目の引数が同じ式木になっています。)
SimpleCheck( Make.Expression((int x) => 0), Expression.Lambda<Func<int, int>>( Expression.Constant(0), // Body intX) // Paremters[0] );
ちなみに、ラムダ式中にさらに式木が含まれていた場合、 その式木は Quote で囲まれます。
SimpleCheck(
Make.Expression(() =>
(Expression<Func<int>>)(() => 0)
).Body,
Expression.Convert(
Expression.Quote(
(Expression<Func<int>>)(() => 0)),
typeof(Expression<Func<int>>))
);
表2: Lambda, Paramter, Constant, Quote
| 対応するコード | 生成メソッド | NodeType | 型 |
|---|---|---|---|
| ラムダ式 | Lambda | Lambda | LambdaExpression(とその派生クラス) |
| 定数 | Constant | Constant | ConstantExpression |
| パラメータ | Parameter | Parameter | ParameterExpression |
| 式木 | Quote | Quote | UnaryExpression |
+ や - などの C# 組込み演算子には、それぞれ対応する式木があります。
サンプル: ExpressionTest.cs 中の ArithmeticUnaryOperator() メソッド。
算術演算には、オーバーフローのチェックを行うかどうかで2つのバージョンがあります。
SimpleCheck( Make.Expression((int x) => -x).Body, Expression.Negate(intX) ); SimpleCheck( Make.Expression((int x) => checked(-x)).Body, Expression.NegateChecked(intX) );
int などに単項 + を適用すると、最適化されて + が消えてしまうので注意。 ユーザ定義型の + の場合はちゃんと + が残ります。
// ↓これは最適化がかかって +x が x になる。 SimpleCheck( Make.Expression((int x) => +x).Body, intX ); SimpleCheck( Make.Expression((CustomUnaryPlus x) => +x).Body, Expression.UnaryPlus(Expression.Parameter(typeof(CustomUnaryPlus), "x")) );
表3: 単項算術演算
| 対応するコード | 生成メソッド | NodeType | 型 |
|---|---|---|---|
| 単項 + | UnaryPlus | UnaryPlus | UnaryExpression |
| 単項 - | Negate | Negate | UnaryExpression |
| checked(-x) | NegateChecked | NegateChecked | UnaryExpression |
サンプル: ExpressionTest.cs 中の ArithmeticBinaryOperator() メソッド。
単項 - と同じく、+, -, * にはオーバーフローをチェックするかどうかで2バージョンあります。
ちなみに、C# の言語仕様では、オーバーフローのチェックを行うのは整数に対してのみです。 double などの不動小数点数では、たとえ checked がついていても、オーバーフローのチェックは行われません。
// たとえ checked がついていても、 // double 同士の演算はオーバーフローをチェックしない SimpleCheck( Make.Expression((double x, double y) => checked(x + y)).Body, Expression.Add( Expression.Parameter(typeof(double), "x"), Expression.Parameter(typeof(double), "y")) );
あと、C# には、べき乗算子はありませんが、 式木にはべき乗を表す Power ノードがあります。 (VB などではべき乗演算子があるため。) (ユーザ定義型で、べき乗の意味で ^ 演算子をオーバーロードしても、 ^ の式木への変換結果は ExclusiveOr になります。)
表4: 2項算術演算(unchecked)
| 対応するコード | 生成メソッド | NodeType | 型 |
|---|---|---|---|
| 加算 + | Add | Add | BinaryExpression |
| 減算 - | Subtract | Subtract | BinaryExpression |
| 乗算 * | Multiply | Multiply | BinaryExpression |
| 除算 / | Divide | Divide | BinaryExpression |
| 剰余 % | Modulo | Modulo | BinaryExpression |
| べき乗(C# には対応する演算子なし) | Power | Power | BinaryExpression |
表5: 2項算術演算(checked)
| 対応するコード | 生成メソッド | NodeType | 型 |
|---|---|---|---|
| checked(+) | AddChecked | AddChecked | BinaryExpression |
| checked(-) | SubtractChecked | SubtractChecked | BinaryExpression |
| checked(*) | MultiplyChecked | MultiplyChecked | BinaryExpression |
サンプル: ExpressionTest.cs 中の ComparisonOperator() メソッド。
表6: 比較演算
| 対応するコード | 生成メソッド | NodeType | 型 |
|---|---|---|---|
| == | Equal | Equal | BinaryExpression |
| != | NotEqual | NotEqual | BinaryExpression |
| < | LessThan | LessThan | BinaryExpression |
| <= | LessThanOrEqual | LessThanOrEqual | BinaryExpression |
| > | GreaterThan | GreaterThan | BinaryExpression |
| >= | GreaterThanOrEqual | GreaterThanOrEqual | BinaryExpression |
サンプル: ExpressionTest.cs 中の LogicalOperator() メソッド。
通常の &, |, ^ がそれぞれ And, Or, ExclusiveOr で、 短絡評価版 &&, || がそれぞれ AndAlso, OrElse です。
bool に対する論理否定 ! と、整数型に対するビット反転 ^ はいずれも Not になります。
表7:
| 対応するコード | 生成メソッド | NodeType | 型 |
|---|---|---|---|
| 論理積 & | And | And | BinaryExpression |
| 論理和 | | Or | Or | BinaryExpression |
| 排他的論理和 ^ | ExclusiveOr | ExclusiveOr | BinaryExpression |
| 論理否定 !・ビット反転 ^ | Not | Not | UnaryExpression |
| 短絡評価 And && | AndAlso | AndAlso | BinaryExpression |
| 短絡評価 Or || | OrElse | OrElse | BinaryExpression |
サンプル: ExpressionTest.cs 中の OtherOperator() メソッド。
ヌル結合演算子 a ?? b は、a != null ? a : b には展開されるわけではなく、 ちゃんと Coalesce という式木ノードがあります。
大半の演算子は 生成メソッド名と NodeType の名前が一致するのに、 条件演算子は微妙に違うので注意。
表8: その他の2項・3項演算
| 対応するコード | 生成メソッド | NodeType | 型 |
|---|---|---|---|
| 左シフト << | LeftShift | LeftShift | BinaryExpression |
| 右シフト >> | RightShift | RightShift | BinaryExpression |
| ヌル結合演算 ?? | Coalesce | Coalesce | BinaryExpression |
| 条件演算子 ? : | Condition | Conditional | ConditionalExpression |
サンプル: ExpressionTest.cs 中の () メソッド。
int から short にキャストする際などには、オーバーフローが発生する可能性があるので、 キャストには算術演算と同様に checked 版と unchecked 版があります。 (as 演算子はそういう挙動はしないので、checked 版なし。)
表9: 型変換・判定
| 対応するコード | 生成メソッド | NodeType | 型 |
|---|---|---|---|
| as | TypeAs | TypeAs | UnaryExpression |
| is | TypeIs | TypeIs | TypeBinaryExpression |
| キャスト | Convert | Convert | UnaryExpression |
| checked キャスト | ConvertChecked | ConvertChecked | UnaryExpression |
サンプル: ExpressionTest.cs 中の MemberAccess() メソッド。
フィールド(メンバ変数)・プロパティの参照が MemberAcess、 配列の長さの参照が ArrayLength、 配列の要素参照が ArrayIndex です。
配列の長さ参照は、C# では Length プロパティの参照で表しますが、 言語によっては配列長参照演算子があるからか、ArrayLength というノードタイプが用意されています。 (配列の Length プロパティの参照は、MemberAccess ではなく ArrayLength になります。)
表10:
| 対応するコード | 生成メソッド | NodeType | 型 |
|---|---|---|---|
| フィールド・プロパティ参照 | MakeMemberAccess | MemberAccess | MemberExpression |
| 配列長参照 | ArrayLength | ArrayLength | UnaryExpression |
| 配列要素参照 | ArrayIndex | ArrayIndex | BinaryExpression |
サンプル: ExpressionTest.cs 中の New() メソッド。
new Point(1, 2) みたいな普通のコンストラクタ呼び出しは New になります。
new int[] { 1, 2 } のような形式の配列生成は ArrayNewInit、 new int[2] のような形式のものは ArrayBounds です。
new Point { X = 1, Y = 2 } のような、イニシャライザを使った初期化は MemberInit になります。 MemberInit ノードは New プロパティと Bindings プロパティを持っていて、 New がコンストラクタ呼び出し、Bindings がイニシャライザによるメンバ初期化を表します。
表11: インスタンス生成
| 対応するコード | 生成メソッド | NodeType | 型 |
|---|---|---|---|
| コンストラクタ呼び出し | New | New | NewExpression |
| 配列(要素指定) | NewArrayInit | NewArrayInit | NewArrayExpression |
| 配列(配列長指定) | NewArrayBounds | NewArrayBounds | NewArrayInit |
| イニシャライザによる初期化 | MemberInit | MemberInit | MemberInitExpression |
MemberInit の Bindings は、 以下のような単純なものは MemberAssingment(Expressin.Bind メソッドで生成)、
new Point { X = 1, Y = 2 }
以下のような、再帰構造を持つものは MemberMemberBinding(Expression.MemberBind で生成)、
new LineSegment
{
Start = { X = 1, Y = 1 },
End = { X = 2, Y = 2 }
}
以下のようなリスト形式のものは ListBinding(Expression.ListBind で生成)
new Polyline { Vertices = { new Point{ X = 1, Y = 1 }, new Point{ X = 2, Y = 2 }, } }
になります。
サンプル: ExpressionTest.cs 中の Call() メソッド。
メソッドの呼び出しは Call、デリゲート・ラムダ式の呼び出しは Invoke になります。
表12:
| 対応するコード | 生成メソッド | NodeType | 型 |
|---|---|---|---|
| メソッド呼び出し | Call | Call | MethodCallExpression |
| デリゲート呼び出し | Invoke | Invoke | InvocationExpression |