概要

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);

ここでは、 どういうラムダ式を書くと、どういう式木が得られるのかを簡単に説明していきます。

サンプルコード →

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 で導入されたオブジェクト初期化子(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 がそれぞればらばらで、少し複雑なんですが、 いくつか先に例を示します。

Expression 型の例
対応するコード 生成メソッド 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 などの型を定義します →

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>>))
);
Lambda, Paramter, Constant, Quote
対応するコード 生成メソッド

ラムダ式

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 になります。)

2項算術演算(unchecked)
対応するコード 生成メソッド

加算 +

Add

BinaryExpression

減算 -

Subtract

BinaryExpression

乗算 *

Multiply

BinaryExpression

除算 /

Divide

BinaryExpression

剰余 %

Modulo

BinaryExpression

べき乗(C# には対応する演算子なし)

Power

BinaryExpression

2項算術演算(checked)
対応するコード 生成メソッド

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 の名前が一致するのに、 条件演算子は微妙に違うので注意。

その他の2項・3項演算
対応するコード 生成メソッド 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 のような、初期化子を使った初期化は 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 の時から変わっていなくて、 単文のラムダ式しか式木にできません。

式木の利用例(リンク)

このサイト内にある式木関連のサンプルにリンク: