這個做法算是做法 30 的詳細說明版本,主要在把 Query Expressions 背後的機制說明一下與它轉換的過程。
在做法 30 有提到基本上每個 Query Syntax
的寫法都有對應的 Method Call Syntax
,更詳細的說不管哪種寫法最後都會轉換成 Method Call,
還有學習到 query expression
的基本語法。
例如下面這段就是使用 query expression
的一段範例程式碼。
var foo = (from n in Enumerable.Range(0, 100)
select n * n).ToArray();
當我們寫完一段 query expression
之後會進行轉換,而這個轉換會根據 query expression pattern
裡面提供的方法來轉換成 Method Call,
這個 pattern 裡面有以下幾種方法讓編譯器挑選最適當的方法後進行轉換。
class C<T> : C
{
public C<T> Where(Func<T, bool> predicate);
public C<U> Select<U>(Func<T, U> selector);
public C<V> SelectMany<U, V>(Func<T, C<U>> selector,
Func<T, U, V> resultSelector);
public C<V> Join<U, K, V>(C<U> inner, Func<T, K> outerKeySelector,
Func<U, K> innerKeySelector, Func<T, U, V> resultSelector);
public C<V> GroupJoin<U, K, V>(C<U> inner, Func<T, K> outerKeySelector,
Func<U, K> innerKeySelector, Func<T, C<U>, V> resultSelector);
public O<T> OrderBy<K>(Func<T, K> keySelector);
public O<T> OrderByDescending<K>(Func<T, K> keySelector);
public C<G<K, T>> GroupBy<K>(Func<T, K> keySelector);
public C<G<K, E>> GroupBy<K, E>(Func<T, K> keySelector,
Func<T, E> elementSelector);
}
class O<T> : C<T>
{
public O<T> ThenBy<K>(Func<T, K> keySelector);
public O<T> ThenByDescending<K>(Func<T, K> keySelector);
}
class G<K, T> : C<T>
{
public K Key { get; }
}
也就是說我們寫一段 query expression
背後會被轉換成這 11 個方法的其中幾個。
Where, Select, SelectMany, Join, GroupJoin, OrderBy, OrderByDescending, ThenBy, ThenByDescending, GroupBy, and Cast
目前有按照 query expression pattern
這個模式進行實作的類型有 IEnumerable<T>
與 IQueryable<T>
,
System.Linq.Enumerable 幫 IEnumerable<T>
提供了豐富的擴充方法來實踐這個模式,System.Linq.Queryable 也有幫 IQueryable<T>
提供
一組類似功能的擴充方法,但它背後使用的是 query provider
的技術讓它有能力把 query expression
轉換成另一種執行格式,
例如 LINQ to SQL 就是把我們寫的 Linq 語法轉換成 SQL 語法,基本上我們使用的 LINQ 語法都會是者兩種其中之一。
接下來依次解讀這段查詢語句背後的意義。
void Main()
{
int[] numbers = { 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 元素依次賦值給 range variable(範圍變數) n, where
子句會產生一個過濾器並且會轉換成
上面提到的 11 個方法中的 Where
方法,會轉換成像下面這樣的格式。
numbers.Where(n => n < 5);
Where
方法需要傳入一個 predicate 委派這個在之前的做法有提到過,它會把符合條件的元素進行輸出。
最後是 select
子句在通常有兩個情況,第一個是像上面的範例中的 select n
因為它選取的是 where
子句所產生的結果集合,
也就是說輸入跟輸出的集合是不相同的,這種特例情況 select
子句就不會轉換成 Select
方法,而是直接被優化省略掉了。
所以最後的結果如下所示
var smallNumbers = numbers.Where(n => n < 5);
第二種則是輸入跟輸出的集合是相同的,這樣 select
子句就會跟預期的一樣轉換成 Select
方法。
var allNumbers = from n in numbers
select n;
var allNumbers = numbers.Select(n => n);
這個轉換流程背後編譯器會先把 query expression
轉換成 method call
,這也是第一步驟所以編譯器這時並不知道
要使用 method call
中的哪一個多載方法可以選用和不管類型的綁定工作,等到把所有的 query expression
的每一個步驟都轉換完成後
才會搜尋挑選最好的多載方法。
select
子句通常可以拿來對過濾後的結果進行二次操作例如下面這個例子,一個會把 where
過濾完的資料進行平方運算,
另一個會使用原本的元素並產生一個新的資料類型集合。
void Main()
{
int[] numbers = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var smallNumbers = from n in numbers
where n < 5
select n * n;
var squares = from n in numbers
select new { Number = n, Square = n * n };
}
接下來討論 orderby
相關的子句,這邊同樣也進行過濾後把結果按照順序進行排序,跟排序相關的方法有這幾個 OrderBy, OrderByDescending, ThenBy, ThenByDescending
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
後面,因為它會把排序完的結果往下傳遞,並且它會把整理後的資料打上標記(markers)
這樣 ThenBy
就會知道 sorted subranges
該處理的範圍有多大。
要注意下面的寫法會與上面的不同,因為下面使用的是三個 orderby
子句因此資料會先根據 LastName
進行完整排序,之後再根據 FirstName
排序最後又根據 Age
排序。
var people = from e in employees
where e.Age > 30
orderby e.LastName
orderby e.FirstName
orderby e.Age
select e;
也可以使用 descending 進行修飾,可以將集合進行到序排序。
var people = from e in employees
where e.Age > 30
orderby e.LastName descending, e.FirstName, e.Age
select e;
下一個是 group 的相關操作,這個語句通常會包含分組操作與多個 from 子句,因此會需要經過多個步驟才能轉換成對應的 Method Call。
下面這個例子就會進行多步驟的轉換。
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()
};
最後才轉換成 Method Call。
var results = employees.GroupBy(e => e.Department).
Select(d => new {
Department = d.Key,
Size = d.Count()
});
(5 items)•••
DepartmentΞΞ SizeΞΞ
0 1
1 2
2 2
3 2
4 2
10 9
上面的語句會會傳一個分組過的集合,下面的語句則會產生新分組元素的集合。
var results = from e in employees
group e by e.Department into d
select new
{
Department = d.Key,
Employees = d.AsEnumerable()
};
var results2 = employees.GroupBy(e => e.Department).
Select(d => new {
Department = d.Key,
Employees = d.AsEnumerable()
});
最後是 SelectMany, Join, GroupJoin 這三種方法的例子,這些方法通常會操作一個以上的集合, 例如下面這個例子就是把兩個集合進行處理後輸出一個新集合。
void Main()
{
int[] odds = { 1, 3, 5, 7 };
int[] evens = { 2, 4, 6, 8 };
var pairs = from oddNumber in odds
from evenNumber in evens
select new
{
oddNumber,
evenNumber,
Sum = oddNumber + evenNumber
};
}
(16 items)•••
oddNumberΞΞ evenNumberΞΞ SumΞΞ
1 2 3
1 4 5
1 6 7
1 8 9
3 2 5
3 4 7
3 6 9
3 8 11
5 2 7
5 4 9
5 6 11
5 8 13
7 2 9
7 4 11
7 6 13
7 8 15
64 80 144
背後會轉換成 SelectMany 方法,它會需要兩個參數第一個參數是用來把 odds
陣列的每個元素對應到 evens
陣列上,跟兩個 foreach 迴圈意思差不多,
第二個參數是會創建兩個集合中各自的元素值選擇器。
int[] odds = { 1, 3, 5, 7 };
int[] evens = { 2, 4, 6, 8 };
var values = odds.SelectMany(oddNumber => evens,
(oddNumber, evenNumber) =>
new
{
oddNumber,
evenNumber,
Sum = oddNumber + evenNumber
});
背後的邏輯大概是像下面這段方法,第一個參數是原始集合,第二個參數會把第一個參數傳給 inputSelector 並得出第二個集合, 最後把第一個集合的元素與第二個集合的元素傳給 resultSelector 進行處理。
static IEnumerable<TOutput> SelectMany<T1, T2, TOutput>(
this IEnumerable<T1> src,
Func<T1, IEnumerable<T2>> inputSelector,
Func<T1, T2, TOutput> resultSelector)
{
foreach (T1 first in src)
{
foreach (T2 second in inputSelector(first))
yield return resultSelector(first, second);
}
}
如果在這種多個 from 所組成的語句,則會使用多個 SelectMany 方法將多個結果串接起來。
var triples = from n in new int[] { 1, 2, 3 }
from s in new string[] { "one", "two", "three"}
from r in new string[] { "I", "II", "III" }
select new { Arabic = n, Word = s, Roman = r };
var numbers = new int[] { 1, 2, 3 };
var words = new string[] { "one", "two", "three" };
var romanNumerals = new string[] { "I", "II", "III" };
var triples = numbers.SelectMany(n => words,
(n, s) => new { n, s })
.SelectMany(pair => romanNumerals,
(pair, n) => new { Arabic = pair.n, Word = pair.s, Roman = n }
);
join 子句會先看語句中是否有包含 into 子句,如果有的話會轉換成 GroupJoin,不包含 into 子句則轉換成 Join。
var numbers = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var labels = new string[] { "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 query = numbers.Join(labels, num => num.ToString(),
label => label, (num, label) => new { num, label });
into 子句會直接把結果進行分組,並且把這些組放在同一個集合中。
var groups = from p in projects
join t in tasks on p equals t.Parent
into projTasks
select new { Project = p, projTasks };
var groups = projects.GroupJoin(tasks,
p => p, t => t.Parent, (p, projTasks) =>
new { Project = p, TaskList = projTasks });
Summary
這個做法在講 query expression pattern
裡面提供的方法背後轉換的邏輯與運作方式,但這些工作基本上都是交由編譯器自動處理,
只要你寫的類型實現了 IEnumerable<T>
那麼使用者就能用 query expression 完成日常操作。