延遲執行
對於多數的查詢操作符來說, 他們並不是在構造後被立即執行, 而是當枚舉發生的時候, 換句話說就是當它對應的枚舉器上的MoveNext被調用的時候. 例如下面的查詢:
1: var numbers = new List<int>( );
2: numbers.Add (3);
3:
4: IEnumerable<int> q = numbers.Select (n => n + 2);
5: numbers.Add (5);
6:
7: foreach (int n in q)
8: Console.Write (n + ","); //5,7,
可以發現我們在查詢構造之後插入的數字也包含在結果之中, 因為只有當foreach表達式運行的時候過濾或者排序才會發生. 我們把它稱為延遲執行. 幾乎所有的標准查詢操作符都具有延遲執行的能力, 但以下這些是例外:
1. 那些返回單一元素或者單一值得操作符, 例如First或者Count
2. 轉換操作符: ToArray, ToList, ToDictionary, ToLookup
這些操作符會被立即執行因為他們的返回類型沒有任何的機制來提供延遲執行. 例如Count, 返回一個簡單的整數類型, 沒有辦法被枚舉. 例如下面的查詢會被立即執行:
1: int result = numbers.Where (n => n <6).Count();
2: Console.WriteLine(result); //2
延遲執行非常重要是因為它將查詢構造和執行解耦了. 這允許你可以分幾步構造一個查詢, 並使LINQ to SQL變得可能.
重估值
延遲執行還帶來另外一個後果, 就是當你重新枚舉的時候延遲執行的查詢將會被重新計算.
1: var numbers = new List<int>( ) { 5, 6 };
2:
3: IEnumerable<int> q = numbers.Select (n => n + 10);
4: foreach (int n in q)
5: Console.Write (n + ","); // 15,16,
6:
7: numbers.Clear( );
8: foreach (int n in q)
9: Console.Write (n + ","); // nothing
有幾個理由可以解釋為什麼重估有些時候會帶來一些不利的影響:
1. 有些時候你想在一個特定的點及時凍結或者緩存結果
2. 有些查詢是密集計算(或者依賴於一個遠程數據庫), 因此我們不想做一些不必要的重復.
要避免重估我們可以調用一個轉換操作符, 例如ToArray或者ToList. ToArray將一個輸出序列拷貝到一個數組, ToList則是將其拷貝到一個泛型的List<>:
1: var numbers = new List<int>( ) { 5, 6, 7 };
2:
3: List<int> r = numbers
4: .Select (n => n + 10)
5: .ToList( );
6:
7: numbers.Clear( );
8: Console.WriteLine (r.Count); //3
外部變量
如果查詢語句中的Lambda表達式引用了本地變量, 這些變量的值將是查詢被執行時候的值, 而不是第一次被捕獲時候的值, 例如:
1: int[] numbers = { 10, 20 };
2: int factor = 10;
3: var query = numbers.Select (n => n * factor);
4: factor = 20; // Change value
5: foreach (int n in query)
6: Console.Write (n + ","); // 200,400,
當我們使用一個foreach循環創建一個查詢的時候這可能會是一個陷阱, 例如:
1: IEnumerable<char> query = "Not what you might expect";
2: foreach (char vowel in "aeiou")
3: {
4: char temp = vowel;
5: query = query.Where (c => c != temp); //如果使用vowel, 那麼將只有’u’會被刪除
6: }
延遲執行是如何工作的?
查詢操作符通過返回裝飾過的序列來提供延遲執行功能.
與傳統的集合類( 例如Array或者Linked List)不同的是, 一個裝飾過的序列本身並沒有自己的數據結構來存儲元素, 相反的, 它包裝了你在運行時提供的序列, 因此, 它保持了一個永久的依賴. 任何時候你從裝飾器中請求數據, 它都會將請求轉發到包裝過的輸入序列中去.
調用Where操作符的時候只是構造了裝飾序列, 並保持了輸入序列, Lambda表達式和其他參數的引用. 只有當裝飾器被枚舉的時候輸入序列才會被枚舉.
例如:
1: IEnumerable<int> lessThanTen =
2: new int[] { 5, 12, 3 }.Where (n => n < 10);
當你枚舉lessThanTen的時候, 你實際上是在通過where裝飾器查詢數組. 一個好消息是你可以通過一個C#迭代器來實現裝飾序列從而編寫你自己的查詢操作符. 以下演示了怎麼編寫你自己的Select方法:
1: static IEnumerable<TResult> Select<TSource,TResult> (
2: this IEnumerable<TSource> source,
3: Func<TSource,TResult> selector)
4: {
5: foreach (TSource element in source)
6: yield return selector (element);
7: }
此方法是利用了yield return表達式的優點返回了一個迭代器, 以下是一個相同表達的快捷版本:
1: static IEnumerable<TResult> Select<TSource,TResult> (
2: this IEnumerable<TSource> source,
3: Func<TSource,TResult> selector)
4: {
5: return new SelectSequence (source, selector);
6: }
在這裡, SelectSequence是一個有編譯器生成的類型,其枚舉器將邏輯裝入迭代方法中.因此, 當你調用一個操作符例如Select或者Where的時候, 你實際上僅僅是實例化了一個包裝了輸入序列的可枚舉類型, 僅此而已.
鏈式裝飾器
連接多個查詢操作符可以用來創建一個多層的裝飾器. 考慮下面的查詢:
1: IEnumerable<int> query = new int[] { 5, 12, 3 }
2: .Where (n => n < 10)
3: .OrderBy (n => n)
4: .Select (n => n * 10);
每一個查詢操作符實例化了一個新的裝飾器,這個裝飾器又包裝了之前的序列. 當你枚舉query的時候, 你是在查詢最初的數組, 並讓其穿過一個多層的鏈式裝飾器來完成轉換.
另外, 延遲執行的一個特性就是如果你使用漸進式的方式組建你的查詢, 也可以創建一個完全等同的對象模型:
1: IEnumerable<int>
2: source = new int[] { 5, 12, 3 },
3: filtered = source .Where (n => n < 10),
4: sorted = filtered .OrderBy (n => n),
5: query = sorted .Select (n => n * 10);
查詢是怎麼執行的?
這是枚舉之前的查詢得到的結果:
foreach (int n in query)
Console.Write (n + ","); // 30,50,
在幕後, foreach調用了Select裝飾器上的GetEnumberator(最外面的操作符),然後開始觸發所有的事件. 結果就是一個鏈式枚舉器結構上對應到一個鏈式的裝飾器序列.
一個查詢就是一個生產線的傳送帶, 我們可以說一個LINQ查詢就是一個懶惰的生產線, 傳送帶和lambda工人根據需求來生產各個元素. 構造一個查詢就是在構造一個生產線 – 我們具備了所有的一切條件– 但還沒有生產任何的東西. 當消費者請求一個元素的時候(枚舉查詢結果), 最右邊的傳送帶激活, 然後觸發其他的傳送帶開始生產 –這時需要提供一個輸入序列. LINQ使用了一個需求驅動的拉模型, 而不是一個供應驅動的推模型. 這對於LINQ to SQL是非常重要的. 待續!