[C#][ラムダ式][式木] Expression の構造を調べてみる

Expression

匿名メソッドとラムダ式の違い」と云う記事で、匿名メソッドとラムダ式の意味の違いについて考えた。

「ラムダ式を Expression として扱っている場合は、匿名メソッドは代わりにはならない」と述べたが、ラムダ式を Expression として扱う例について、これから数回に分けて書いていきたい。

ラムダ式を Expression として扱う例を挙げる前に、今回は、先ずは Expression 自体について理解を深めたいと思う。

Expression はどのような構造をしているのだろうか?
中がどうなっているのかを見てみることにしよう。

■ Expression をデバッガーで観察してみよう

手始めに、Visual Studio のデバッガーを用いて Expression の中を覗いてみる。

ラムダ式として足し算を行うだけの (x, y) => x + y と云うシンプルなものを用意し、これを Expression で受ける。

using System;
using System.Linq.Expressions;
class Program
{
static void Main()
{
Expression<Func<int, int, int>> expression = (x, y) => x + y;
}
}

これをデバッグ実行して、デバッガーで expression の中を覗いてみる。

デバッガーで expression の中を覗いてみる 1

Body、Name、NodeType、Parameters、Type 等の各プロパティとその値が見えているのが判るだろう。

Name はなく、NodeType は Lambda だ。名前がないと云うことと、式の種類がラムダ式であることを表している。
Type は Func`3、つまり TResult Func<in T1, in T2, out TResult> になっている。

Body の中を見てみよう。

デバッガーで expression の中を覗いてみる - Body

ラムダ式の中の => より後ろの x + y の部分であることが判る。引数の x が見えている。NodeType は Add で足し算の式であることを表している。
Type は Int32、つまり
int だ。
Left は x、Right は y となっていて、二項演算である足し算の左オペランドが x、右オペランドが y であることを表している。

更に Left の中を見てみる。

デバッガーで expression の中を覗いてみる - Body - Left

Name は x、Type は Int32 つまり int。
NodeType は Parameter で式の種類が引数であることを表している。

続いて Parameters。

デバッガーで expression の中を覗いてみる - Parameters

Parameters は要素数 2 のコレクションになっているようだ。ラムダ式の中の => より前の部分の引数を表していることが判る。

Parameters の一つ目の要素を見てみよう。

デバッガーで expression の中を覗いてみる - Parameters の一つ目の要素

引数の x が見えている。NodeType は Parameter つまり式の種類は引数、Type は Int32 つまり int だ。

■ Expression はツリー構造

上の例では、expression のNodeType は Lambda であり、この式の種類がラムダ式であることを表していた。

その他にも、幾つかの式が出てきている。

例えば、Body の部分。ラムダ式の中の => より後ろの x + y の部分だが、ここも式であり、式の種類は二項演算の足し算の式であった。

デバッガーでよく見てみると、これらの式も Expression であることが判る。

Expression はその中に再帰的に Expression を持つことでツリー構造になっているようだ。

■ Expression の種類

Expression には幾つかの種類があることも判る。

ラムダ式、二項演算の式、などだ。

実は、Expression クラスには沢山の派生クラスがあり、それぞれが様様な式の種類を表している。

以下を参照してほしい:

その一部を示すと、こんな感じだ。

Expression クラスの派生クラス 種類 NodeType プロパティの値
NodeType NodeType の説明
LambdaExpression ラムダ式 Lambda ラムダ式
BinaryExpression 二項演算式 Add 足し算
AddChecked オーバーフロー チェックを行う足し算
Subtract 引き算
SubtractChecked オーバーフロー チェックを行う引き算
Multiply 掛け算
MultiplyChecked オーバーフロー チェックを行う掛け算
Divide 割り算
Modulo 剰余演算
Power 累乗
UnaryExpression 単項演算式 Negate 単項マイナス演算
NegateChecked オーバーフロー チェックを行う単項マイナス演算
UnaryPlus 単項プラス演算
Not ビット補数演算または論理否定演算
Convert キャスト演算
ConvertChecked チェック付きキャスト演算
TypeAs as による型変換
ArrayLength 配列の長さを取得する演算
Quote 定数式

■ Expression のツリー構造を見てみよう

それでは、Expression のツリー構造を実際に見てみよう。

ツリー構造になった Expression を再帰的に辿りながら表示していくクラスを作ってみた。

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq.Expressions;
static class EnumerableExtensions
{
// コレクションの各要素に対して、指定された処理をインデックス付きで実行
public static void ForEach<TItem>(this IEnumerable<TItem> collection, Action<TItem, int> action)
{
int index = 0;
foreach (var item in collection)
action(item, index++);
}
}
// Expression の中身をダンプ (ラムダ式、二項演算式、単項演算式以外は簡易表示)
static class ExpressionViewer
{
// Expression の中を (再帰的に) 表示
public static void Show(this Expression expression, int level = 0)
{
if (expression as LambdaExpression != null)
// ラムダ式のときは詳細に表示
ShowLambdaExpression((LambdaExpression)expression, level);
else if (expression as BinaryExpression != null)
// 二項演算のときは詳細に表示
ShowBinaryExpression((BinaryExpression)expression, level);
else if (expression as UnaryExpression != null)
// 単項演算のときは詳細に表示
ShowUnaryExpression((UnaryExpression)expression, level);
else if (expression != null)
// それ以外も沢山あるが、今回は省略してベース部分だけ表示
ShowExpressionBase(expression, level);
}
// Expression のベース部分を表示
static void ShowExpressionBase(Expression expression, int level)
{
ShowText(string.Format("☆Expression: {0}", expression), level);
ShowText(string.Format("ノードタイプ: {0}", expression.NodeType), level + 1);
}
// LambdaExpression (ラムダ式) の中を (再帰的に) 表示
static void ShowLambdaExpression(LambdaExpression expression, int level)
{
ShowExpressionBase(expression, level);
ShowText(string.Format("名前: {0}", expression.Name), level + 1);
ShowText(string.Format("戻り値の型: {0}", expression.ReturnType), level + 1);
ShowParameterExpressions(expression.Parameters, level + 1); // 引数のコレクション
ShowText(string.Format("本体: {0}", expression.Body), level + 1);
expression.Body.Show(level + 2); // 本体を再帰的に表示
}
// BinaryExpression (二項演算式) の中を (再帰的に) 表示
static void ShowBinaryExpression(BinaryExpression expression, int level)
{
ShowExpressionBase(expression, level);
ShowText(string.Format("型: {0}", expression.Type), level + 1);
ShowText(string.Format("左オペランド: {0}", expression.Left), level + 1);
expression.Left.Show(level + 2); // 左オペランドを再帰的に表示
ShowText(string.Format("右オペランド: {0}", expression.Right), level + 1);
expression.Right.Show(level + 2); // 右オペランドを再帰的に表示
}
// UnaryExpression (単項演算式) の中を (再帰的に) 表示
static void ShowUnaryExpression(UnaryExpression expression, int level)
{
ShowExpressionBase(expression, level);
ShowText(string.Format("型: {0}", expression.Type), level + 1);
ShowText(string.Format("オペランド: {0}", expression.Operand), level + 1);
expression.Operand.Show(level + 2); // オペランドを再帰的に表示
}
// 引数の式のコレクションを表示
static void ShowParameterExpressions(ReadOnlyCollection<ParameterExpression> parameterExpressions, int level)
{
ShowText("引数群", level);
if (parameterExpressions == null || parameterExpressions.Count == 0)
ShowText("引数なし", level);
else
parameterExpressions.ForEach((parameterExpression, index) => ShowParameterExpression(parameterExpression, index, level + 1));
}
// 引数の式の中を表示
static void ShowParameterExpression(ParameterExpression parameterExpression, int index, int level)
{
ShowText(string.Format("引数{0}", index + 1), level + 1);
ShowExpressionBase(parameterExpression, level + 1);
ShowText(string.Format("引数の型: {1}, 引数の名前: {2}", parameterExpression.NodeType, parameterExpression.Type, parameterExpression.Name), level + 2);
}
// 文字列をレベルに応じてインデント付で表示
static void ShowText(string itemText, int level)
{
Console.WriteLine("{0}{1}", Indent(level), itemText);
}
// インデントの為の文字列を生成
static string Indent(int level)
{
return level == 0 ? "" : new string(' ', (level - 1) * 4 + 1) + "|-- ";
}
}

この ExpressionViewer クラスを使って、先程の expression の中を見てみよう。

class Program
{
public static void Main()
{
Expression<Func<int, int, int>> expression = (x, y) => x + y;
((Expression)expression).Show();
}
}

実行してみると、こうなる。

☆Expression: (x, y) => (x + y)
|-- ノードタイプ: Lambda
|-- 名前:
|-- 戻り値の型: System.Int32
|-- 引数群
|-- 引数1
|-- ☆Expression: x
|-- ノードタイプ: Parameter
|-- 引数の型: System.Int32, 引数の名前: x
|-- 引数2
|-- ☆Expression: y
|-- ノードタイプ: Parameter
|-- 引数の型: System.Int32, 引数の名前: y
|-- 本体: (x + y)
|-- ☆Expression: (x + y)
|-- ノードタイプ: Add
|-- 型: System.Int32
|-- 左オペランド: x
|-- ☆Expression: x
|-- ノードタイプ: Parameter
|-- 右オペランド: y
|-- ☆Expression: y
|-- ノードタイプ: Parameter

「☆Expression」とあるところが式 (Expression) だ。ツリー構造になっている。

「引数群」のところでは、x と y がそれぞれ引数という種類の式としてネストしている。

「本体」は x + y の部分で、足し算を表す式としてネストしている。
その足し算の左オペランドである x と右オペランドである y が、それぞれ引数タイプの式として更にネストしている。

次に、ちょっとだけラムダ式を複雑にしてみよう。(x, y) => x + y を (x, y, z) => x + y + z に変えてみる。

こうだ。

class Program
{
public static void Main()
{
Expression<Func<int, int, int, int>> expression = (x, y, z) => x + y + z;
((Expression)expression).Show();
}
}

さて、結果はどう変化するだろうか。

☆Expression: (x, y, z) => ((x + y) + z)
|-- ノードタイプ: Lambda
|-- 名前:
|-- 戻り値の型: System.Int32
|-- 引数群
|-- 引数1
|-- ☆Expression: x
|-- ノードタイプ: Parameter
|-- 引数の型: System.Int32, 引数の名前: x
|-- 引数2
|-- ☆Expression: y
|-- ノードタイプ: Parameter
|-- 引数の型: System.Int32, 引数の名前: y
|-- 引数3
|-- ☆Expression: z
|-- ノードタイプ: Parameter
|-- 引数の型: System.Int32, 引数の名前: z
|-- 本体: ((x + y) + z)
|-- ☆Expression: ((x + y) + z)
|-- ノードタイプ: Add
|-- 型: System.Int32
|-- 左オペランド: (x + y)
|-- ☆Expression: (x + y)
|-- ノードタイプ: Add
|-- 型: System.Int32
|-- 左オペランド: x
|-- ☆Expression: x
|-- ノードタイプ: Parameter
|-- 右オペランド: y
|-- ☆Expression: y
|-- ノードタイプ: Parameter
|-- 右オペランド: z
|-- ☆Expression: z
|-- ノードタイプ: Parameter

一段ネストが深くなったのがお判りだろうか。

x + y + z のところが (x + y) + z、即ち「『x + y という足し算の式』を左オペランドとし、z を右オペランドとする足し算の式」になっている。

では、更にネストを深くする為に、もっと複雑なラムダ式を使ってみよう。

「デリゲートを入力としデリゲートを出力とするラムダ式」でやってみる。

例えばこんなやつ。

Func<Func<double, double, double>, Func<int, int, int>> convertFunc = func => ((x, y) => (int)func((double)x, (double)y));

余談だが、一応このラムダ式について説明してみる。

このラムダ式は、「int 二つを引数とし int を返すメソッド」を「double 二つを引数とし double を返すメソッド」に変換する。
double を対象とした足し算を int を対象とした足し算に変換したり、double
を対象とした引き算を int を対象とした引き算に変換したりできることになる。

余り意味のない例だが、こんな感じだ。

using System;
static class Program
{
static double Add(double x, double y)
{
return x + y;
}
static double Subtract(double x, double y)
{
return x - y;
}
public static void Main()
{
Func<Func<double, double, double>, Func<int, int, int>> convertFunc = func => ((x, y) => (int)func((double)x, (double)y));
int answer1 = convertFunc(Add )(1, 2);
int answer2 = convertFunc(Subtract)(4, 3);
}
}

このラムダ式を ExpressionViewer クラスを使ってダンプしてみよう。

class Program
{
public static void Main()
{
Expression<Func<Func<double, double, double>, Func<int, int, int>>> expression = func => ((x, y) => (int)func((double)x, (double)y));
((Expression)expression).Show();
}
}

結果は下のようになる。

☆Expression: func => (x, y) => Convert(Invoke(func, Convert(x), Convert(y)))
|-- ノードタイプ: Lambda
|-- 名前:
|-- 戻り値の型: System.Func`3[System.Int32,System.Int32,System.Int32]
|-- 引数群
|-- 引数1
|-- ☆Expression: func
|-- ノードタイプ: Parameter
|-- 引数の型: System.Func`3[System.Double,System.Double,System.Double], 引数の名前: func
|-- 本体: (x, y) => Convert(Invoke(func, Convert(x), Convert(y)))
|-- ☆Expression: (x, y) => Convert(Invoke(func, Convert(x), Convert(y)))
|-- ノードタイプ: Lambda
|-- 名前:
|-- 戻り値の型: System.Int32
|-- 引数群
|-- 引数1
|-- ☆Expression: x
|-- ノードタイプ: Parameter
|-- 引数の型: System.Int32, 引数の名前: x
|-- 引数2
|-- ☆Expression: y
|-- ノードタイプ: Parameter
|-- 引数の型: System.Int32, 引数の名前: y
|-- 本体: Convert(Invoke(func, Convert(x), Convert(y)))
|-- ☆Expression: Convert(Invoke(func, Convert(x), Convert(y)))
|-- ノードタイプ: Convert
|-- 型: System.Int32
|-- オペランド: Invoke(func, Convert(x), Convert(y))
|-- ☆Expression: Invoke(func, Convert(x), Convert(y))
|-- ノードタイプ: Invoke

「引数群」のところは、単に一つのデリゲートになっている。

「本体」のところは、ラムダ式がネストしている。
そして、ネストしたラムダ式は、通常のラムダ式と同じようにツリー構造に展開されている。

■ 今回のまとめ

と云う訳で、今回は Expression の構造を見てみた。

次回以降で、愈愈ラムダ式を Expression として利用する例を紹介したい。