延遲加載與延遲求值查詢


原文在我的博客中,排版更舒服哦:http://www.dozer.cc/2012/07/lazy-load-and-lazy-evaluation-queries/

 

對延遲加載的片面認識

很多人對延遲加載的初步認識就是,在使用 LINQ for Entity 的時候,查詢語句不會立即執行查詢,只有在使用 foreach 或者 ToList() 等方法的時候,才會去查詢數據庫。 那如果我用的不是 LINQ for IQueryable,而是 LINQ for IEnumerable(前者往往是查詢遠程數據的,后者查詢的都是內存數據),例如自己的一些數據庫訪問層,返回的數據就是 List<T>,內存已經在數據中了,是不是就沒有延遲加載了呢? 非也!  

 

延遲加載的實現原理

LINQ for IQueryable 查詢的往往是遠程數據的,當你調用 Where(),Single() 等方法的時候,並沒有去查詢數據庫,而是保存為表達式樹了。 只有當你使用 foreach 或者 ToList() 等方法的時候,才會把之間的所有表達式轉換成 SQL 或其他查詢方法,然后和遠程數據交互。 具體原理可以查看下來文章:http://blogs.msdn.com/b/mattwar/archive/2008/11/18/linq-links.aspx  LINQ for IEnumerable 是針對於內存中數據的查詢語句,數據既然已經在內存中了,那么還需要延遲加載嗎? 其實,這時候准確的說應該叫延遲求值查詢(Lazy Evaluation Queries),而不是延遲加載。總之,它們還是有區別的!

var list = new List<int>{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var result1 = list.Where(_ => _ < 5).Where(_ => _ != 8);
var result2 = list.Where(_ => _ < 5).ToList().Where(_ => _ != 8);

先看上述代碼,大家覺得執行 result1 和 result2 的時候有什么區別嗎? 其實區別不大,后者多了 ToList() 方法。但是,他們在性能上有很大的差別! 拋開 ToList() 在類型轉換上的損耗(其實這里沒有類型轉換,沒什么損耗),從執行過程上,前者只是把 list 循環了一次,而 后者把 list 循環了2次! 所以,在使用 LINQ 的時候,如果返回的是一個集合,強烈建議不要調用 ToList() 方法,數據在最終使用的時候也只不過是用 foreach 來調用,出於便捷性和性能的考慮,調用 ToList() 都不是一個好的決策。但是為什么第一行查詢只會跑一次循環呢?  

 

迭代器與延遲求值查詢

大家有沒有發現在這個章節我把延遲加載改成了延遲求值查詢?因為嚴格的來說,LINQ for IEnumerable 查詢的數據已經在內存中了,那還需要加載什么呢? 另外,理解原理后,大家也會明白,這里其實是延遲求值查詢,而不是延遲加載。 延遲求值查詢指的是:對集合調用 Where() 等方法后,並不會立刻進行循環。只有當對集合調用 foreach 或 ToList() 等方法的時候,才會真正地進行循環,並且只會循環一次。 關於這個問題,《More Effective C#》中的 item32:Prefer Lazy Evaluation Queries 解釋地非常詳細了。大家也可以搜索到中文版。   簡單地來說,LINQ 技術中,所有對於 IEnumerable 的擴展方法都使用了迭代器,所以多次調用這幾個方法並不會進行多次循環,而是會合並成一個循環。 具體原理可以查看本書原文,也可以買中文版看,或到這里查看網友的翻譯。  

 

延遲加載和延遲求值查詢的思考

由於之前的片面認識,導致我一直認為延遲加載只有在使用 LINQ to Entity 等 ORM 框架的時候,才會有用。 不僅我是這樣,相信很多人在寫 BLL 層輸出數據的時候,也都是用 List<T> 作為輸出類型的。   所以往往在網站中是這么設計的:

  • BLL層:在對數據源進行各種操作,排序、篩選、分頁等,最后輸出的時候用 ToList() 方法輸出;如果不是 Entity Framework 等 ORM 框架的話,也直接輸出 List<T>。
  • WEB層:把數據源傳遞給前端頁面,用 foreach 在頁面上輸出。

  上述步驟看似好,其實這樣的設計也沒什么問題。性能也很不錯! 在 BLL 層對同一個數據源調用各種方法,期間並不真正地調用數據庫,而是在最后調用一次數據庫。 理論上,把數據輸出到頁面或者 controller 后,不應該有任何邏輯代碼了,但實際上,還是有可能會在這幾個地方修改集合的;最關鍵的是,就算如此,你也額外多做了一次循環。 一次是在 BLL 層 ToList() 的時候,一次是在頁面上 foreach 的時候。   但是大家看微軟 http://asp.net/ 上的例子后就會發現,上面的例子從來不會調用 ToList() 方法,所有的輸出類型都是 IEnumerable 或者 IQueryable,在頁面上也只不過是使用 foreach 操作。   所以,在將來的項目中,推薦大家在 BLL 層返回的類型都是 IEnumerable<T>;如果用的是 LINQ for IQueryable,例如 Entity Framework 技術,應該把返回類型寫為 IQueryable<T>。 因為如果把 Entity Framework 查詢出來的集合(類型是 IQueryable<T> )轉換為 IEnumerable<T> 的話,再次調用任何方法,就不會把操作存放在表達式樹中了。具體可以看下一個章節。

 

IQueryable<T> 顯示轉換為 IEnumerable<T> 時出現的問題

LINQ 技術中,為 IQueryable<T> 接口和 IEnumerable<T> 寫了兩套擴展方法(LINQ 技術中的各種函數使用擴展方法實現的)。 所以,雖然 IQueryable<T> 繼承於 IEnumerable<T>,當把前者的顯示類型轉換成后者的時候,調用同樣的函數(其實只是名字看起來相同,底層和方法的參數都不同)的執行過程是不一樣的。 它們內部實現方法並不同,IQueryable<T> 的實現方法是表達式樹,IEnumerable<T> 的實現方法是迭代器。 所以如果這么做的話,當類型為 IQueryable<T> 時,相應的操作會保存為表達式樹;IEnumerable<T> 時,操作會保存為枚舉器。也就是說,這時一半為延遲查詢、一半為延遲求值查詢了。這樣的性能當然沒有全部是延遲查詢來得好。  

 

自己實現延遲求值查詢

另外,如果你沒用用 LINQ,老版本 .NET?或者是需要一些復雜的操作 .NET 無法滿足怎么辦? 怎么樣才能和 LINQ 提供的查詢方法一樣,利用迭代器實現一次循環而非多次循環呢? 請看如下代碼:

static void Main(string[] args)
{
    var list = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    var result = Test2(Test1(list));
    Console.WriteLine("這里並沒有輸出!");
    result.ToList();
}

static IEnumerable<int> Test1(IEnumerable<int> list)
{
    foreach (var l in list)
    {
        Console.WriteLine(l + " in Test1");
        yield return l;
    }
}

static IEnumerable<int> Test2(IEnumerable<int> list)
{
    foreach (var l in list)
    {
        Console.WriteLine(l + " in Test2");
        yield return l;
    }
}

執行結果如下:

 

如果這兩個方法是一般的方法實現,那么,在執行第二條語句的時候,result 就已經有值了,而且也會在控制台有輸出。 另外,也應該是先循環 Test1 方法,再循環 Test2 方法,所以輸出的結果應該先全部是 "Test1" ,然后再是 "Test2"。   但實際執行結果卻很不同,在執行第二條語句的時候,沒有有任何輸出,代表並沒有執行任何代碼,而只是以迭代器的形式存放了起來。 而在使用 ToList() 的時候,總共進行了一個循環,對每一個元素分別調用 Test1 和 Test2 中的代碼。 可見,你只需要把以前方法的返回類型改成 IEnumerable<T> ,並利用 yield return 輸出元素即可。 PS.當然,如果用的是 Entity Framework 等 LINQ for IQueryable<T> 技術,類型最好應該為 IQueryable<T>。

 

不適用場景

這種方法雖然能在特定的場景下提升性能,但是並不是適合所有場景。因為利用此方法后,相當於把集合中的元素一個個執行對應的方法,最終合並成了一個循環。 很明顯,語義被破壞了,所以在這么些的時候,一定要保證各個元素之間沒有關聯,或者沒有整體關聯。如果有相應的關系,恐怕會影響最終的結果。 但是,LINQ 用了這么久,真沒出現過因為延遲查詢而影響執行結果的情況,可使用這類方法的時候,還是要注意是否會影響影響最終結果!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM