++C++; // 未確認飛行 C

Google
Web ufcpp.net

式木(Expression Trees)

目次

キーワード

概要

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.csExpressionTest.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 型

前節の例でちょこっと出てきた Expression.Lambda や Expression.Add メソッドによって生成されるのは、 LambdaExpression 型や BinaryExpression 型の変数になりますが、 これらは全て、Expression 型の派生クラスになります。

Expression 型の派生クラスは、直接 new することはできず、 Expression 型の static メソッド(Lambda や Add)を使って生成します。

Expression 型は NodeType というプロパティを持っていて、 例えば、加算なら NodeType == ExpressionType.Add になります。

生成用の static メソッド、 具体的な型、 NodeType がそれぞればらばらで、少し複雑なんですが、 いくつか先に例を示します。

表1: Expression 型の例

対応するコード生成メソッドNodeType
+AddAddBinaryExpression
newNewNewNewExpression
() => 0Lambda<Func<int>>LambdaLambdaExpression<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
ラムダ式LambdaLambdaLambdaExpression(とその派生クラス)
定数ConstantConstantConstantExpression
パラメータParameterParameterParameterExpression
式木QuoteQuoteUnaryExpression

算術演算

+ や - などの 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
単項 +UnaryPlusUnaryPlusUnaryExpression
単項 -NegateNegateUnaryExpression
checked(-x)NegateCheckedNegateCheckedUnaryExpression

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

表4: 2項算術演算(unchecked)

対応するコード生成メソッドNodeType
加算 +AddAddBinaryExpression
減算 -SubtractSubtractBinaryExpression
乗算 *MultiplyMultiplyBinaryExpression
除算 /DivideDivideBinaryExpression
剰余 %ModuloModuloBinaryExpression
べき乗(C# には対応する演算子なし)PowerPowerBinaryExpression

表5: 2項算術演算(checked)

対応するコード生成メソッドNodeType
checked(+)AddCheckedAddCheckedBinaryExpression
checked(-)SubtractCheckedSubtractCheckedBinaryExpression
checked(*)MultiplyCheckedMultiplyCheckedBinaryExpression

比較演算

サンプル: ExpressionTest.cs 中の ComparisonOperator() メソッド。

表6: 比較演算

対応するコード生成メソッドNodeType
==EqualEqualBinaryExpression
!=NotEqualNotEqualBinaryExpression
<LessThanLessThanBinaryExpression
<=LessThanOrEqualLessThanOrEqualBinaryExpression
>GreaterThanGreaterThanBinaryExpression
>=GreaterThanOrEqualGreaterThanOrEqualBinaryExpression

論理演算

サンプル: ExpressionTest.cs 中の LogicalOperator() メソッド。

通常の &, |, ^ がそれぞれ And, Or, ExclusiveOr で、 短絡評価版 &&, || がそれぞれ AndAlso, OrElse です。

bool に対する論理否定 ! と、整数型に対するビット反転 ^ はいずれも Not になります。

表7:

対応するコード生成メソッドNodeType
論理積 &AndAndBinaryExpression
論理和 |OrOrBinaryExpression
排他的論理和 ^ExclusiveOrExclusiveOrBinaryExpression
論理否定 !・ビット反転 ^NotNotUnaryExpression
短絡評価 And &&AndAlsoAndAlsoBinaryExpression
短絡評価 Or ||OrElseOrElseBinaryExpression

その他の2項・3項演算

サンプル: ExpressionTest.cs 中の OtherOperator() メソッド。

ヌル結合演算子 a ?? b は、a != null ? a : b には展開されるわけではなく、 ちゃんと Coalesce という式木ノードがあります。

大半の演算子は 生成メソッド名と NodeType の名前が一致するのに、 条件演算子は微妙に違うので注意。

表8: その他の2項・3項演算

対応するコード生成メソッドNodeType
左シフト <<LeftShiftLeftShiftBinaryExpression
右シフト >>RightShiftRightShiftBinaryExpression
ヌル結合演算 ??CoalesceCoalesceBinaryExpression
条件演算子 ? :ConditionConditionalConditionalExpression

型変換・判定

サンプル: ExpressionTest.cs 中の () メソッド。

int から short にキャストする際などには、オーバーフローが発生する可能性があるので、 キャストには算術演算と同様に checked 版と unchecked 版があります。 (as 演算子はそういう挙動はしないので、checked 版なし。)

表9: 型変換・判定

対応するコード生成メソッドNodeType
asTypeAsTypeAsUnaryExpression
isTypeIsTypeIsTypeBinaryExpression
キャストConvertConvertUnaryExpression
checked キャストConvertCheckedConvertCheckedUnaryExpression

メンバ参照

サンプル: ExpressionTest.cs 中の MemberAccess() メソッド。

フィールド(メンバ変数)・プロパティの参照が MemberAcess、 配列の長さの参照が ArrayLength、 配列の要素参照が ArrayIndex です。

配列の長さ参照は、C# では Length プロパティの参照で表しますが、 言語によっては配列長参照演算子があるからか、ArrayLength というノードタイプが用意されています。 (配列の Length プロパティの参照は、MemberAccess ではなく ArrayLength になります。)

表10:

対応するコード生成メソッドNodeType
フィールド・プロパティ参照MakeMemberAccessMemberAccessMemberExpression
配列長参照ArrayLengthArrayLengthUnaryExpression
配列要素参照ArrayIndexArrayIndexBinaryExpression

インスタンス生成

サンプル: 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
コンストラクタ呼び出しNewNewNewExpression
配列(要素指定)NewArrayInitNewArrayInitNewArrayExpression
配列(配列長指定)NewArrayBoundsNewArrayBoundsNewArrayInit
イニシャライザによる初期化MemberInitMemberInitMemberInitExpression

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
メソッド呼び出しCallCallMethodCallExpression
デリゲート呼び出しInvokeInvokeInvocationExpression
Transtation into English

[お問い合わせ](q)