Chapter 4 - Item 37 : Prefer Lazy Evaluation to Eager Evaluation in Queries

Effective C# (Covers C# 6.0), (includes Content Update Program): 50 Specific Ways to Improve Your C#, 3rd Edition By Bill Wagner 讀後心得

作者提出兩種取得集合枚舉的方式,分別為 Lazy Evaluation(延後取得)、Eager Evaluation(立即取得)。兩種方式各有其適用情境,一般而言, Lazy Evaluation 會適用於大部分的情境。

首先必須了解 Lazy Evaluation 的產生方式,以下用一個簡單的範例演示。

public static IEnumerable<TResult> Generate<TResult>( 
int number, 
Func<TResult> generator )
{
    for ( int i = 0; i < number; i++ )
        yield return generator( );
}

測試程式碼:

Debug.WriteLine( $"Start time for Test one : {DateTime.Now:T}" );
var sequence = Generate( 10, ( ) => DateTime.Now );

Debug.WriteLine( "Interating..." );

foreach ( var value in sequence )
    Debug.WriteLine( $"{value:T}" );

Debug.WriteLine( "Waiting..." );
Thread.Sleep( 1000 );

Debug.WriteLine( "Interating..." );

foreach ( var value in sequence )
    Debug.WriteLine( $"{value:T}" );

輸出:

Start time for Test one : 下午 01:27:06
Interating...
下午 01:27:06
下午 01:27:06
下午 01:27:06
下午 01:27:06
下午 01:27:06
下午 01:27:06
下午 01:27:06
下午 01:27:06
下午 01:27:06
下午 01:27:06
Waiting...
Interating...
下午 01:27:07
下午 01:27:07
下午 01:27:07
下午 01:27:07
下午 01:27:07
下午 01:27:07
下午 01:27:07
下午 01:27:07
下午 01:27:07
下午 01:27:07

由此例可得知,每一次呼叫 Generate 都將產生新的集合枚舉(由 Time Stamp 可看出);也就是說,兩次的訪問是互相獨立的,每一次的訪問都會即時產生新的元素。

另外值得注意的一點,query expressions 理論上可以訪問一個長度為無限大的集合枚舉。在設計 query 時,需留意是否可在組合訪問表達式時;早期的限制訪問範圍區間,避免效能上的浪費。 以下用一個簡單的例子演示。

public static IEnumerable<int> AllNumbers( )
{
    var number = 0;

    while ( number < int.MaxValue )
        yield return number++;
}

寫法一:加入 Take 限制只回傳前 10 個元素。

var answers = from number in AllNumbers( )
              select number;

var smallNumbers = answers.Take( 10 );

foreach ( var num in smallNumbers )
    Debug.WriteLine( num.ToString( ) );

寫法二:利用 Where 繞行檢查整個集合枚舉。

ar answers = from number in AllNumbers( )
             where number < 10
             select number;

foreach ( var num in answers )
    Debug.WriteLine( num.ToString( ) );

顯然寫法二會造成不必要的效能浪費,where 語句需繞行檢查整個集合;而因為原始集合已經排序(升冪排序),使用 Take 限制回傳元素數量會是比較好的選擇。

再來看一個例子。

var products = new List<Product>( );

// Order before filter.
var sortedProductsSlow = from p in products
                         orderby p.UnitInStock descending
                         where p.UnitInStock > 100
                         select p;

// Filter before order.
var sortedProductsFast = from p in products
                         where p.UnitInStock > 100
                         orderby p.UnitInStock descending
                         select p;

orderby 語句需要繞行整個集合枚舉,若是在 where 語句前執行,會對不必要的元素做排序;反之則能夠先經過 where 語句篩選,只排序需要的元素,大大減輕系統負擔。

結論:
 1. Lazy Evaluation 可滿足大部分的情境。

2. Eager Evaluation 可利用呼叫 ToList( ) 或 ToArray( ) 立即取得集合;通常用於需要集合快照或是該集合需進行多次不同操作,減少取得資料的成本(e.g. SQL Database, 網路傳輸.)。