延遲查詢
在運行 LINQ 中的某些擴展方法進行集合的查詢時,查詢不會立即運行。只有當運行至 foreach 對查詢結果進行遍歷時,或者對查詢結果調用 ToList() 方法等情況時,查詢纔會真正的運行。
我們以 Where()
方法爲例進行研究。考察如下代碼:
var names = new List<string> {"Nino", "Alberto", "Juan", "Mike", "Phil"};
var namesWithJ = names.Where(n=>n.StartsWith("J")).OrderBy(n => n);
Console.WriteLine("第一次查詢");
foreach (string name in namesWithJ)
{
Console.WriteLine(name);
}
Console.WriteLine();
names.Add("John");
names.Add("Jim");
names.Add("Jack");
names.Add("Denny");
Console.WriteLine("第二次查詢");
foreach (string name in namesWithJ)
{
Console.WriteLine(name);
}
輸出結果如下:
第一次查詢
Juan
第二次查詢
Jack
Jim
John
Juan
我們的查詢只定義了一次,兩次查詢卻有不同的輸出。這是因爲,在遇到 foreach 迭代查詢結果時,Where 查詢纔去從數據源中查詢數據,返回給調用者。
現在,我們在查詢語句後面加一個 ToList()
方法,那麼兩次查詢輸出的都是 Juan
了。
yield return 語句
我們研究一下 Where()
方法,發現它返回的是一個 IEnumerable<T>
對象。它是個枚舉器。也就是說,我們的查詢語句並沒有在內存中產生一個集合,而是一個枚舉器。
Where()
方法的內部實現原理如下:
public static IEnumerable<T> Where<T> (this IEnumerable<T> source, Func<T, bool> predicate)
{
foreach (T item in source)
{
if (predicate(item))
yield return item;
}
}
yield
語句是 C# 2.0 添加的用於方便創建枚舉器的關鍵字。yield return
語句返回集合的一個元素,並移動到下一個元素上。 從 foreach 中依次訪問每一項時,就會調用枚舉器進行枚舉。這樣就可以迭代大量的數據,而無需一次把所有的數據都讀入內存中。迭代器方法運行到 yield return 語句時,會返回一個 expression,並保留當前在代碼中的位置。 下次調用迭代器函數時,將從該位置重新開始執行。
Enumerator 接口(或其泛型形式)定義了一個狀態機。它內部擁有一個成員記錄當前的狀態(當前遍歷到的元素 Current),擁有 MoveNext() 方法和 Reset() 方法,這樣就可以不斷迭代下去。
返回一個枚舉器而不是一個集合,這樣做有一些好處。例如,如果我們要從數據庫中查詢一億個數據,然後將這些數據輸出。如果將查詢結果放到集合中,然後再輸出,那麼我們不得不等到這一億個數據全部查寫入內存中後才能輸出,這樣就有一個阻塞的過程。而如果通過枚舉器,那麼在執行輸出動作的時候,纔會真正從數據庫中進行查詢,並且沒查詢到一個數據,就可以立即輸出它,然後接着從數據庫中查詢下一條。這樣,用戶會覺得數據的顯示沒有延遲。
參考文獻
[1] Where 方法
[2] IEnumerator 接口
[3] 迭代器
[4] yield 參考
[5] C#中yield return用法分析