[C#][ラムダ式][LINQ][式木] 匿名メソッドとラムダ式の違い

Expression

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

■ 同じように使える匿名メソッドとラムダ式

匿名メソッドとラムダ式は、同じように使うことができる場面が多い。

例えば、以下のようなデリゲートを引数にとるメソッドがあったとして、

using System;
using System.Collections.Generic;
static class Enumerable
{
// 述語に基づいて値のシーケンスをフィルター処理
public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
foreach (var item in source) {
if (predicate(item))
yield return item;
}
}
}

この predicate には、通常のメソッドも匿名メソッドもラムダ式も渡すことができ、いずれも同じ結果となる。

using System;
class Program
{
static bool IsEven(int number)
{
return number % 2 == 0;
}
static void Main()
{
var numbers = new[] { 3, 7, 1, 6, 4, 5 };
// 普通のメソッドをデリゲートに入れて渡す例
var evenNumbers1_1 = numbers.Where(new Func<int, bool>(IsEven));
// 上に同じ
var evenNumbers1_2 = numbers.Where(IsEven);
// 匿名メソッドを使った例
var evenNumbers2 = numbers.Where(delegate(int number) { return number % 2 == 0; });
// ラムダ式を使った例
var evenNumbers3 = numbers.Where(number => number % 2 == 0);
}
}

いずれの場合も、同じ型のデリゲートとして渡されることになるのだ。

以下の例でも同じ型のデリゲートとして受けられることが判る。

Func<int, bool> delegate1_1 = new Func<int, bool>(IsEven);
Func<int, bool> delegate1_2 = IsEven; // 上と同じ
Func<int, bool> delegate2 = delegate(int number) { return number % 2 == 0; };
Func<int, bool> delegate3 = number => number % 2 == 0;

上の例では、一行目 delegate1_1 の例と二行目 delegate1_2 の例は全く同じ意味だ。書き方が違うだけの、単なる糖衣構文 (syntax sugar) に過ぎない。

では、三行目の匿名メソッドを使った delegate2 の例 と四行目のラムダ式を使った delegate3 の例も、単なる糖衣構文 (syntax sugar) なのだろうか?

確かに、ラムダ式を使った方が、型推論の恩恵を存分に受けられ、書き方がぐっとシンプルになる。だが、書き方だけの違いなのだろうか?

■ 匿名メソッドとラムダ式で挙動が異なる例

今度は、両者で違いが出る例を見てみよう。

LINQ to SQL を使った例だ。

予め SQL Server に Employee というシンプルなテーブルを用意した。

Employee テーブル

次に、Visual Studio でプロジェクトを作り、EmployeeDataClasses.dbml という名前で「LINQ to SQL クラス」を追加した。

「LINQ to SQL クラス」を追加
・LINQ to SQL – ラムダ式で書いた場合

では、ラムダ式を使ってデータ アクセスを行う例から見てみよう。

// LINQ to SQL の例 - ラムダ式で書いた場合
using LambdaExpressionSample; // EmployeeDataClassesDataContext の名前空間
using System;
using System.Linq;
class Program
{
static void Main()
{
var dataContext = new EmployeeDataClassesDataContext();
dataContext.Log = Console.Out; // 発行された SQL をモニターする為に、コンソールに出力
var data1 = dataContext.Employee.Where(employee => employee.Name.Contains("山"));
var data2 = data1.OrderBy(employee => employee.Name);
var data3 = data2.Select(employee => employee.Name);
data3.ToList().ForEach(Console.WriteLine);
}
}

この例で発行された SQL は、以下の通り。

SELECT [t0].[Name]
FROM [dbo].[Employee] AS [t0]
WHERE [t0].[Name] LIKE @p0
ORDER BY [t0].[Name]
-- @p0: Input NVarChar (Size = 4000; Prec = 0; Scale = 0) [%山%]

三行に分けて書いているのに、SQL の発行は data3.ToList() のときの一回だけだ。data1、data2、data3 と三回も戻り値を受けているのに、ToList() されるまで評価が遅延されている訳だ。

SQL の発行は一回だけだが、where は WHERE、OrderBy は ORDER BY、Select で Name だけなのは SELECT で Name だけと、ちゃんと狙い通りのものが発行されているのが判る。

LINQ to SQL (や Entity Framework) の凄いところだ。

因みに、上ではメソッド構文を使って書いているが、クエリ構文を使って書くこともできる。

以下の二つは意味的には同じで、糖衣構文 (syntax sugar) だ。

// メソッド構文
var data3 = dataContext.Employee
.Where(employee => employee.Name.Contains("山"))
.OrderBy(employee => employee.Name)
.Select(employee => employee.Name);
// クエリ構文
var data3 = from employee in dataContext.Employee
where employee.Name.Contains("山")
orderby employee.Name
select employee.Name;
・LINQ to SQL – 匿名メソッドで書いた場合

次に、この例を匿名メソッドに置き換えてやってみよう。ラムダ式が匿名メソッドの単なる糖衣構文 (syntax sugar) なら同じ結果になる筈だ。

// LINQ to SQL の例 - 匿名メソッドで書いた場合
using LambdaExpressionSample;
using System;
using System.Linq;
class Program
{
static void Main()
{
var dataContext = new EmployeeDataClassesDataContext();
dataContext.Log = Console.Out; // 発行された SQL をモニターする為に、コンソールに出力
var data1 = dataContext.Employee.Where(delegate(Employee employee) { return employee.Name.Contains("山"); });
var data2 = data1.OrderBy(delegate(Employee employee) { return employee.Name; });
var data3 = data2.Select(delegate(Employee employee) { return employee.Name; });
data3.ToList().ForEach(Console.WriteLine);
}
}

結果はこうなった。

SELECT [t0].[Id], [t0].[Name]
FROM [dbo].[Employee] AS [t0]

ラムダ式の場合と大きく異なる。

WHERE による行の絞り込みも SELECT による列の絞り込みも ORDER BY による並べ替えも全然反映されていない。

つまり、この例では、LINQ to SQL で単純に Employee テーブルの全列、全行を取ってきて、後は LINQ to Object でオンメモリで処理している訳だ。

これでは全く非効率だ。ラムダ式を使うべき、と云うことになろう。

・LINQ to SQL – ラムダ式と匿名メソッドを混在させた場合

序でに、両者を混在させた例も見ておこう。

// LINQ to SQL の例 - ラムダ式と匿名メソッドを混在させた場合
using LambdaExpressionSample; // EmployeeDataClassesDataContext の名前空間
using System;
using System.Linq;
class Program
{
static void Main()
{
var dataContext = new EmployeeDataClassesDataContext();
dataContext.Log = Console.Out; // 発行された SQL をモニターする為に、コンソールに出力
var data1 = dataContext.Employee.Where(employee => employee.Name.Contains("山"));
var data2 = data1.OrderBy(delegate(Employee employee) { return employee.Name; });
var data3 = data2.Select(employee => employee.Name);
data3.ToList().ForEach(Console.WriteLine);
}
}

結果はこうなった。

SELECT [t0].[Id], [t0].[Name]
FROM [dbo].[Employee] AS [t0]
WHERE [t0].[Name] LIKE @p0
-- @p0: Input NVarChar (Size = 4000; Prec = 0; Scale = 0) [%山%]

SQL に反映されているのは、ラムダ式を使った Where までで、匿名メソッドより後ろは LINQ to Object で処理されているのが判る。

この例で、ラムダ式は匿名メソッドと意味的に同じものではない、ということが判った。

■ 匿名メソッドとラムダ式の違い

では、両者はどう意味が違うのだろうか。

上の LINQ to SQL の例の Where の部分を比べてみよう。

// ラムダ式を使った例
var data1 = dataContext.Employee.Where(employee => employee.Name.Contains("山"));
// 匿名メソッドを使った例
var data1 = dataContext.Employee.Where(delegate(Employee employee) { return employee.Name.Contains("山"); });

Visual Studio を使って、両者の Where の部分を右クリックし、「定義へ移動」してみよう。

一見同じ Where メソッドを呼んでいるようだが、異なった Where メソッドを呼んでいることが判るだろう。

// ラムダ式を使った例から呼ばれる Where メソッド
namespace System.Linq
{
public static class Queryable
{
public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate);
}
}
// 匿名メソッドを使った例から呼ばれる Where メソッド
namespace System.Linq
{
public static class Enumerable
{
public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate);
}
}

匿名メソッドの方は Func<TSource, bool>、即ちデリゲートだが、ラムダ式の方は Expression と云う違うもので受けている。

参考までに、Func と Expression の定義は以下のようになっている。

// Func はデリゲート
namespace System
{
public delegate TResult Func<in T, out TResult>(T arg);
}
// Expression はデリゲートではない
namespace System.Linq.Expressions
{
public sealed class Expression<TDelegate> : LambdaExpression;
}

ラムダ式は、あくまでも「式」として渡されているのだ。

対して、匿名メソッドは Expression として扱うことができない。

試してみよう。

using LambdaExpressionSample; // EmployeeDataClassesDataContext の名前空間
using System;
using System.Linq.Expressions;
class Program
{
static void Main()
{
// デリゲートとして使う分には、どちらでも OK
Func<Employee, bool> delegate1 = employee => employee.Name.Contains("山");
Func<Employee, bool> delegate2 = delegate(Employee employee) { return employee.Name.Contains("山"); };
// だが、Expression として使えるのはラムダ式の方だけ
Expression<Func<Employee, bool>> expression1 = employee => employee.Name.Contains("山");
//Expression<Func<Employee, bool>> expression2 = delegate(Employee employee) { return employee.Name.Contains("山"); }; // コンパイル エラーになる
}
}

匿名メソッドの方は、「匿名メソッド式を式のツリーに変換することはできません」と云うコンパイル エラーとなる。

「匿名メソッド式を式のツリーに変換することはできません」と云うコンパイル エラー

即ち、ラムダ式を Expression として扱っている場合は、匿名メソッドは代わりにはならない、ということになる。

ラムダ式は匿名メソッドの糖衣構文 (syntax sugar) などではないのだ。

一般的には、ラムダ式を使うことで書き方もシンプルになることだし、匿名メソッドは使わずにラムダ式で書くようにした方が良いだろう。

■ 今回のまとめ

今回は、匿名メソッドとラムダ式の意味の違いについて考察した。

ラムダ式はデリゲートのみならず、式としても扱うことができる、と云うことだ。

LINQ to SQL のような LINQ プロバイダーでは、ときにラムダ式を式として扱うことが大切だろう、と想像できる。

だが、それ以外のプログラミングでラムダ式を式として扱って便利なことってあるんだろうか?

と云う訳で、次回は、ラムダ式を Expression として扱うと便利な例について書く予定だ。