這個做法算是做法 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 完成日常操作。