「匿名メソッドとラムダ式の違い」と云う記事で、匿名メソッドとラムダ式の意味の違いについて考えた。
「ラムダ式を 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 の中を覗いてみる。
Body、Name、NodeType、Parameters、Type 等の各プロパティとその値が見えているのが判るだろう。
Name はなく、NodeType は Lambda だ。名前がないと云うことと、式の種類がラムダ式であることを表している。
Type は Func`3、つまり TResult Func<in T1, in T2, out TResult> になっている。
Body の中を見てみよう。
ラムダ式の中の => より後ろの x + y の部分であることが判る。引数の x が見えている。NodeType は Add で足し算の式であることを表している。
Type は Int32、つまり
int だ。
Left は x、Right は y となっていて、二項演算である足し算の左オペランドが x、右オペランドが y であることを表している。
更に Left の中を見てみる。
Name は x、Type は Int32 つまり int。
NodeType は Parameter で式の種類が引数であることを表している。
続いて Parameters。
Parameters は要素数 2 のコレクションになっているようだ。ラムダ式の中の => より前の部分の引数を表していることが判る。
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 として利用する例を紹介したい。