目次

キーワード

概要

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 { 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 の時から変わっていなくて、 単文のラムダ式しか式木にできません。

式木の利用例(リンク)

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

更新履歴

ブログ