[C#][ラムダ式][式木] 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 の中を覗いてみる。
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 として利用する例を紹介したい。
ディスカッション
コメント一覧
まだ、コメントがありません