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);
}
}
join
はJoin()
に変換される。
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});
個人的な感想
複数のselect
やjoin
が出てくる場合はクエリ式のほうが簡潔に書けるけど、それ以外の場合はメソッド呼び出しのほうが簡潔に書ける。クエリ式を使う機会はそんなに多くなさそうな気がする。まあ、自分が書かなくても、他の人が書いたものを読む機会はあるので、クエリ式=>メソッド呼び出しの変換規則は、知っておいて損はない。