概要
Ver. 3.0
「ラムダ式」は、Expression 型の変数に代入すると、 匿名デリゲート(実行可能なコード)ではなく式木(式の意味を表す木構造データ)としてコンパイルされます。 例えば、以下の2つのコードは同じ意味になります。
Expression<Func<int, int>> e = x => x + 5
var x = Expression.Parameter(typeof(int), "x");
var e =
Expression.Lambda<Func<int, int>>(
Expression.Add(x, Expression.Constant(5)),
x);
ここでは、 どういうラムダ式を書くと、どういう式木が得られるのかを簡単に説明していきます。
サンプルコード →
式木にできるラムダ式の条件
まず先に、式木を使う上での制約について。 ラムダ式ならば何でも式木にできるというわけではありません。
ラムダ式には、以下に例示するような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 で導入されたオブジェクト初期化子(object initializer)(参考:「初期化子」)を使えば、結構複雑な式も書けたりします。 例えば以下のような感じ。
Expression<Func<LineSegment>> e = () =>
new LineSegment
{
Start = { X = 0, Y = 0 },
End = { X = 1, Y = 1 },
};
Expression 型
前節の例でちょこっと出てきた Expression.Lambda や Expression.Add メソッドによって生成されるのは、 LambdaExpression 型や BinaryExpression 型の変数になりますが、 これらは全て、Expression 型の派生クラスになります。
Expression 型の派生クラスは、直接 new することはできず、 Expression 型の static メソッド(Lambda や Add)を使って生成します。
Expression 型は NodeType というプロパティを持っていて、 例えば、加算なら NodeType == ExpressionType.Add になります。
生成用の static メソッド、 具体的な型、 NodeType がそれぞればらばらで、少し複雑なんですが、 いくつか先に例を示します。
対応するコード | 生成メソッド | NodeType | 型 |
---|---|---|---|
+ | Add | Add | BinaryExpression |
new | New | New | NewExpression |
() => 0 | Lambda<Func<int>> | Lambda | LambdaExpression<Func<int>> |
実装上、ほとんどのものが、生成メソッドの名前と、NodeType 列挙子の名前はそろえてあるようです。 (条件演算子とメンバーアクセスだけ例外。 条件演算子は Expression.Condition で生成するけど、NodeType は Conditional。 メンバーアクセスは Expression.MakeMemberAccess で生成するけど、NodeType は MemberAccess。)
下準備
百聞は一見にしかずということで、 次節以降では、ラムダ式と式木の対応関係を実例を挙げて紹介していきます。 それに先立って、いくつか補助関数や変数を用意しておきます。
まず、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 などの型を定義します →
ラムダ式
サンプル: 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>>))
);
対応するコード | 生成メソッド | 型 |
---|---|---|
ラムダ式 | Lambda | LambdaExpression(とその派生クラス) |
定数 | Constant | ConstantExpression |
パラメータ | Parameter | ParameterExpression |
式木 | 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"))
);
対応するコード | 生成メソッド | 型 |
---|---|---|
単項 + | UnaryPlus | UnaryExpression |
単項 - | Negate | UnaryExpression |
checked(-x) | NegateChecked | UnaryExpression |
2項演算
サンプル: 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 になります。)
対応するコード | 生成メソッド | 型 |
---|---|---|
加算 + | Add | BinaryExpression |
減算 - | Subtract | BinaryExpression |
乗算 * | Multiply | BinaryExpression |
除算 / | Divide | BinaryExpression |
剰余 % | Modulo | BinaryExpression |
べき乗(C# には対応する演算子なし) | Power | BinaryExpression |
対応するコード | 生成メソッド | 型 |
---|---|---|
checked(+) | AddChecked | BinaryExpression |
checked(-) | SubtractChecked | BinaryExpression |
checked(*) | MultiplyChecked | BinaryExpression |
比較演算
サンプル: ExpressionTest.cs 中の ComparisonOperator() メソッド。
対応するコード | 生成メソッド | 型 |
---|---|---|
== | Equal | BinaryExpression |
!= | NotEqual | BinaryExpression |
< | LessThan | BinaryExpression |
<= | LessThanOrEqual | BinaryExpression |
> | GreaterThan | BinaryExpression |
>= | GreaterThanOrEqual | BinaryExpression |
論理演算
サンプル: ExpressionTest.cs 中の LogicalOperator() メソッド。
通常の &, |, ^ がそれぞれ And, Or, ExclusiveOr で、 「短絡評価」版 &&, || がそれぞれ AndAlso, OrElse です。
bool に対する論理否定 ! と、整数型に対するビット反転 ^ はいずれも Not になります。
対応するコード | 生成メソッド | 型 |
---|---|---|
論理積 & | And | BinaryExpression |
論理和 | | Or | BinaryExpression |
排他的論理和 ^ | ExclusiveOr | BinaryExpression |
論理否定 !・ビット反転 ^ | Not | UnaryExpression |
短絡評価 And && | AndAlso | BinaryExpression |
短絡評価 Or || | OrElse | BinaryExpression |
その他の2項・3項演算
サンプル: ExpressionTest.cs 中の OtherOperator() メソッド。
ヌル結合演算子 a ?? b は、a != null ? a : b には展開されるわけではなく、 ちゃんと Coalesce という式木ノードがあります。
大半の演算子は 生成メソッド名と NodeType の名前が一致するのに、 条件演算子は微妙に違うので注意。
対応するコード | 生成メソッド | NodeType | 型 |
---|---|---|---|
左シフト << | LeftShift | LeftShift | BinaryExpression |
右シフト >> | RightShift | RightShift | BinaryExpression |
ヌル結合演算 ?? | Coalesce | Coalesce | BinaryExpression |
条件演算子 ? : | Condition | Conditional | ConditionalExpression |
型変換・判定
サンプル: ExpressionTest.cs 中の () メソッド。
int から short にキャストする際などには、オーバーフローが発生する可能性があるので、 キャストには算術演算と同様に checked 版と unchecked 版があります。 (as 演算子はそういう挙動はしないので、checked 版なし。)
対応するコード | 生成メソッド | 型 |
---|---|---|
as | TypeAs | UnaryExpression |
is | TypeIs | TypeBinaryExpression |
キャスト | Convert | UnaryExpression |
checked キャスト | ConvertChecked | UnaryExpression |
メンバー参照
サンプル: ExpressionTest.cs 中の MemberAccess() メソッド。
フィールド(メンバー変数)・プロパティの参照が MemberAcess、 配列の長さの参照が ArrayLength、 配列の要素参照が ArrayIndex です。
配列の長さ参照は、C# では Length プロパティの参照で表しますが、 言語によっては配列長参照演算子があるからか、ArrayLength というノードタイプが用意されています。 (配列の Length プロパティの参照は、MemberAccess ではなく ArrayLength になります。)
対応するコード | 生成メソッド | 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 が初期化子ーによるメンバー初期化を表します。
対応するコード | 生成メソッド | 型 |
---|---|---|
コンストラクタ呼び出し | New | NewExpression |
配列(要素指定) | NewArrayInit | NewArrayExpression |
配列(配列長指定) | NewArrayBounds | NewArrayInit |
初期化子による初期化 | 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 になります。
対応するコード | 生成メソッド | 型 |
---|---|---|
メソッド呼び出し | Call | MethodCallExpression |
デリゲート呼び出し | Invoke | InvocationExpression |
式木 4.0(構文木)
Ver. 4.0
.NET Framework 4 で、式木が大幅にバージョンアップしました。 式木と言いつつ(Expression クラスではあるものの)、実際には、 複文、条件分岐、ループなども使えるようになっています。
要するに、構文木(syntax tree)相当の機能は揃っています。 これまでとの互換性から式木(expression tree)を名乗っているだけで、 実際には DLR で使っている構文木の全機能を備えています。
以下に、.NET 4 の式木の例を示します。
using System;
using System.Linq.Expressions;
public class Program
{
public static void Main()
{
var x = Expression.Parameter(typeof(int), "x");
var i = Expression.Parameter(typeof(int), "i");
var endLoop = Expression.Label("EndLoop");
var body = Expression.Block(
typeof(int),
new[] { x },
Expression.Assign(x, Expression.Constant(0)),
Expression.Loop(
Expression.Block(
Expression.AddAssign(x, i),
Expression.SubtractAssign(i, Expression.Constant(1)),
Expression.IfThen(
Expression.LessThan(i, Expression.Constant(0)),
Expression.Break(endLoop))),
endLoop),
x);
var e = Expression.Lambda<Func<int, int>>(body, i);
var f = e.Compile();
Console.WriteLine(f(2));
Console.WriteLine(f(4));
Console.WriteLine(f(6));
}
}
これで、以下のコードに相当する式木が作れます。
Func<int, int> f = i =>
{
int x = 0;
for (; ;)
{
x += i;
i -= 1;
if (i <= 0) break;
}
return x;
};
(ループは永久ループに相当する LoopExpression しかなくて、for や while 相当のコードを書くには、上記のように if と break を使います。)
ただし、このラムダ式を Expression<Func<int, int>>
に代入することはできません。
C# の仕様自体は C# 3.0 の時から変わっていなくて、
単文のラムダ式しか式木にできません。
式木の利用例(リンク)
このサイト内にある式木関連のサンプルにリンク: