1. 程式人生 > >linq的延遲執行--學習linq的資料和筆記(四)

linq的延遲執行--學習linq的資料和筆記(四)

延遲執行的實現 是因為使用了IEnumerable<T>的這種列舉進行迭代! 

如:方法

 public static IEnumerable<string> getString()
        {
            for (int i = 0; i < 10; i++)
            {
                yield return "s" + i;
            }
        }
使用的是yield return  而非return

在呼叫時 

 static void Main(string[] args)
        {
          
           var strs= getString();
           foreach (var item in strs)
           {
               Console.WriteLine(item);
           }
            Console.ReadLine();
        }

我們把斷點打在方法迴圈內,和呼叫的遍歷迴圈內!如下


除錯可以看到 在呼叫時 方法並沒有執行,而是在遍歷的時候,每遍歷一個元素,方法內才返回一個元素!也就是延遲執行。

這就是迭代器的作用(在這裡我就不寫迭代器相關的知識了,有時間我會單獨發部落格)

因為linq方法的返回值型別大部分使用了IEnumerable<T>或者實現了IEnumerable<T>,所以linq中的大部分方法 是具有延遲執行的功能的!

以下內容 摘自部落格園(學習資料)

LINQ中大部分查詢運算子都有一個非常重要的特性:延遲執行。這意味著,他們不是在查詢建立的時候執行,而是在遍歷的時候執行(換句話說,當enumerator的MoveNext方法被呼叫時)。讓我們考慮下面這個query:

複製程式碼
         static void TestDeferredExecution()
{
var numbers = new List<int>();
numbers.Add(1);
IEnumerable<int> query = numbers.Select(n => n * 10); // Build query
numbers.Add(2); // Add an extra element after the query foreach
(int n in query)
Console.Write(n + "|"); // 10|20| }
複製程式碼

可以看出,我們在查詢建立之後新增的number也包含在查詢結果中了,這是因為直到foreach語句對query進行遍歷時,LINQ查詢才會執行,這時,資料來源numbers已經包含了我們後來新增的元素2,LINQ的這種特性就是延遲執行。除了下面兩種查詢運算子,所有其他的運算子都是延遲執行的:

  • 返回單個元素或者標量值的查詢運算子,如First、Count等。
  • 下面這些轉換運算子:ToArray、ToList、ToDictionary、ToLookup。

上面兩種運算子會被立即執行,因為他們的返回值型別沒有提供延遲執行的機制,比如下面的查詢會被立即執行。

            int matches = numbers.Where(n => (n % 2) == 0).Count();     // 1

對於LINQ來說,延遲執行時非常重要的,因為它把查詢的建立與查詢的執行解耦了,這讓我們可以向建立SQL查詢那樣,分成多個步驟來建立我們的LINQ查詢。

重複執行

延遲執行帶來的一個影響是,當我們重複遍歷查詢結果時,查詢會被重複執行:

複製程式碼
        static void TestReevaluation()
{
var numbers = new List<int>() { 1, 2 };

IEnumerable<int> query = numbers.Select(n => n * 10); // Build query foreach (int n in query) Console.Write(n + "|"); // 10|20|
numbers.Clear();
foreach (int n in query) Console.Write(n + "|"); // <nothing> }
複製程式碼

有時候,重複執行對我們說可不是一個優點,理由如下:

  • 當我們需要在某一個給定的點儲存查詢的結果時。
  • 有些查詢比較耗時,比如在對一個非常大的sequence進行查詢或者從遠端資料庫獲取資料時,為了效能考量,我們並不希望一個查詢會被反覆執行。

這個時候,我們就可以利用之前介紹的轉換運算子,比如ToArray、ToList來避開重複執行,ToArray把查詢結果儲存至一個Array,而ToList把結果儲存至泛型List<>:

複製程式碼
        static void TestDefeatReevaluation()
{
var numbers = new List<int>() { 1, 2 };

List<int> timesTen = numbers
.Select(n => n * 10)
.ToList(); // Executes immediately into a List<int>
numbers.Clear();
Console.Write(timesTen.Count); // Still 2 }
複製程式碼

變數捕獲

延遲執行還有一個不好的副作用。如果查詢的lambda表示式引用了程式的區域性變數時,查詢會在執行時對變數進行捕獲。這意味著,如果在查詢定義之後改變了該變數的值,那麼查詢結果也會隨之改變。

複製程式碼
        static void TestCapturedVariable()
{
int[] numbers = { 1, 2 };

int factor = 10;
IEnumerable<int> query = numbers.Select(n => n * factor);
factor = 20;
foreach (int n in query)
Console.Write(n + "|"); // 20|40| }
複製程式碼

這個特性在我們通過foreach迴圈建立查詢時會變成一個真正的陷阱。假如我們想要去掉一個字串裡的所有母音字母,我們可能會寫出如下的query:

複製程式碼
              IEnumerable<char> query = "How are you, friend.";

query = query.Where(c => c != 'a');
query = query.Where(c => c != 'e');
query = query.Where(c => c != 'i');
query = query.Where(c => c != 'o');
query = query.Where(c => c != 'u');

foreach (char c in query) Console.Write(c); //Hw r y, frnd.
複製程式碼

儘管程式結果正確,但我們都能看出,如此寫出來的程式不夠優雅。所以我們會自然而然的想到使用foreach迴圈來重構上面這段程式:

            IEnumerable<char> query = "How are you, friend.";

foreach(char vowel in "aeiou")
query = query.Where(c => c != vowel);

foreach (char c in query) Console.Write(c); //How are yo, friend.

結果中只有字母u被過濾了,咋一看,有沒有吃一驚呢!但只要仔細一想就能知道原因:因為vowel定義在迴圈之外,所以每個lambda表示式都捕獲了同一變數。當我們的query執行時,vowel的值是什麼呢?不正是被過濾的字母u嘛。要解決這個問題,我們只需把迴圈變數賦值給一個內部變數即可,如下面的temp變數作用域只是當前的lambda表示式。

複製程式碼
            IEnumerable<char> query = "How are you, friend.";

foreach (char vowel in "aeiou")
{
char temp = vowel;
query = query.Where(c => c != temp);
}

foreach (char c in query) Console.Write(c); //Hw r y, frnd.
複製程式碼

延遲執行的實現原理

查詢運算子通過返回裝飾者sequence(decorator sequence)來支援延遲執行。

和傳統的集合型別如array,linked list不同,一個裝飾者sequence並沒有自己用來存放元素的底層結構,而是包裝了我們在執行時提供的另外一個sequence。此後當我們從裝飾者sequence中請求資料時,它就會轉而從包裝的sequence中請求資料。

比如呼叫Where會建立一個裝飾者sequence,其中儲存了輸入sequence的引用、lambda表示式還有其他提供的引數。下面的查詢對應的裝飾者sequence如圖所示:

        IEnumerable<int> lessThanTen = new int[] { 5, 12, 3 }.Where(n => n < 10);

 當我們遍歷lessThanTen時,實際上我們是在通過Where裝飾者從Array中查詢資料。

而查詢運算子連結建立了一個多層的裝飾者,每個查詢運算子都會例項化一個裝飾者來包裝前一個sequence,比如下面的query和對應的多層裝飾者sequence:

            IEnumerable<int> query = new int[] { 5, 12, 3 }
.Where(n => n < 10)
.OrderBy(n => n)
.Select(n => n * 10);

在我們遍歷query時,我們其實是在通過一個裝飾者鏈來查詢最初的array。

需要注意的是,如果在上面的查詢後面加上一個轉換運算子如ToList,那麼query會被立即執行,這樣,單個list就會取代上面的整個物件模型。