Effective C# 3rd 読書メモ 36 クエリ式がどのようにメソッド呼び出しにマップされるか理解する

LINQは2つの概念に基づいている: クエリ言語と、クエリ言語から一式のメソッドへの翻訳である。C#コンパイラはクエリ言語で書かれたクエリ式をメソッド呼び出しに変換する。

全てのクエリ式は、1つ以上のメソッド呼び出しにマップされる。このマッピングを理解することは、組み込みのクラスを利用する場合にも、組み込みのクラスを拡張する場合にも重要である。

クエリ式のパターンは11種類ある(詳細は割愛)。

.NETの基本ライブラリは以下の実装を提供している。

  • System.Linq.Enumerableによる、IEnumerable<T>の拡張メソッド(クエリ式の実装)
  • System.Linq.Queryableによる、IQueryable<T>の拡張メソッド(クエリを別形式に変換する)

クエリ式からメソッド呼び出しへの変換はコンパイラによって行われる。

var numbers = new[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
var smallNumbers = from n in numbers
                    where n < 5
                    select n;

from n in numbersはnumbersの値を1つずつ取り出してnという変数に紐付ける。where節はWhere()メソッドに変換されるフィルターを定義する。

select節はSelect()メソッドに変換されるが、一定条件のもと最適化によって消去されることがある。サンプルのselectは、範囲のある値(range)を、単に別のrangeに変換しているだけである。このようなselectをdegenerate select(退化select)と呼ぶ。degenerate selectは特別な処理を行っていないので、コンパイラはこのselectを消去する。

この結果、上記クエリ式は以下のメソッド呼び出しに変換される。

var smallNumbers = numbers.Where(n => n < 5);

以下のように、selectが元の値とは異なる値を返す場合には、省略されない。

var squares = from n in numbers
                select n * n;

次に、orderby節を検討する。

var people = from e in employees
                where e.Age > 30
                orderby e.LastName, e.FirstName, e.Age
                select e;

ここで、クエリ式は以下のように変換される。

var people = employees.Where(e => e.Age > 30)
    .OrderBy(e => e.LastName)
    .ThenBy(e => e.FirstName)
    .ThenBy(e => e.Age);

ThenBy()メソッドはOrderBy()ないしThenBy()の結果を受け、追加の並び替えを行う。

残りは、グルーピングと、複数のfrom句の使用による継続(continuations)の実現である。継続を含むクエリ式は、ネストされたクエリに変換され、ネストされたクエリがメソッド呼び出しに変換される。

var results = from e in employees
                group e by e.Department
                into d
                select new
                {
                    Department = d.Key,
                    Size = d.Count()
                };

上記コードは、継続によって以下のように変換される。

var results = from d in
                from e in employees
                group e by e.Department
                select new
                {
                    Department = d.Key,
                    Size = d.Count()
                };

ネストされたクエリができたら、メソッド呼び出しに変換される。

var results = employees.GroupBy(e => e.Department)
    .Select(d => new
    {
        Department = d.Key,
        Size = d.Count()
    });

最後はSelectMany()Join()、そしてGroupJoin()である。

var odds = new[] {1, 3, 5, 7};
var evens = new[] {2, 4, 6, 8};
var pairs = from oddNumber in odds
            from evenNumber in evens
            select new
            {
                oddNumber,
                evenNumber,
                Sum = oddNumber + evenNumber
            };

上記コードでは、16個のペアが作成される。このクエリ式のように、複数のfrom句を含むクエリ式は、SelectMany()メソッドに変換される。

var pairs = odds.SelectMany(oddNumber => evens,
    (oddNumber, evenNumber) =>
        new
        {
            oddNumber,
            evenNumber,
            Sum = oddNumber + evenNumber
        });

なお、SelectMany()の内部では以下のような処理が行われている。

static IEnumerable<TOutput> SelectMany<T1, T2, TOutput>(
    this IEnumerable<T1> src,
    Func<T1, IEnumerable<T2>> inputSelector,
    Func<T1, T2, TOutput> resultSelector)
{
    foreach (var first in src)
    {
        foreach (var second in inputSelector(first))
            yield return resultSelector(first, second);
    }
}

joinJoin()に変換される。

var numbers = new[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
var labels = new[] {"0", "1", "2", "3", "4", "5"};
var query = from num in numbers
            join label in labels on num.ToString() equals label
            select new {num, label};

// 上のクエリ式と等価なメソッド呼び出し
var query2 = numbers.Join(labels, num => num.ToString(),
    label => label, (num, label) => new {num, label});

into節を含むjoin式はGroupJoin()に変換される。

var groups = from p in projects
    join t in tasks on p equals t.Parent
    into projectTasks
    select new {Project = p, projectTasks};

// 上のクエリ式と等価なメソッド呼び出し
var groups2 = projects
    .GroupJoin(tasks, p => p, t => t.Parent,
        (p, projectTasks) => new {Project = p, projectTasks});

個人的な感想

複数のselectjoinが出てくる場合はクエリ式のほうが簡潔に書けるけど、それ以外の場合はメソッド呼び出しのほうが簡潔に書ける。クエリ式を使う機会はそんなに多くなさそうな気がする。まあ、自分が書かなくても、他の人が書いたものを読む機会はあるので、クエリ式=>メソッド呼び出しの変換規則は、知っておいて損はない。

コメントをどうぞ

コメントを残す