Effective C# 33.被請求時產生序列項目 Effective C# 33.被請求時產生序列項目 (Generate Sequence Items as Requested)

Published on Tuesday, October 22, 2024

這個做法也是繼續在對 Iterator Methodsyield return 進行延伸討論,這次是在對 Lazy Evaluation 特性進行更詳細的說明, 還有不使用 Iterator Methods 可能會導致轉換問題。

首先產生集合這個需求是非常常見的,例如在讀取完資料庫後通常需要轉換成可操作的集合,假設我有一個產生集合的方法像下面這段程式碼:

static IList<int> CreateSequence(int numberOfElements, int startAt, int stepBy)
{
	var collection = new List<int>(numberOfElements);
	for (int i = 0; i < numberOfElements; i++)
		collection.Add(startAt + i * stepBy);
	return collection;
}

了解前幾個做法的概念後可以知道,上面這段程式碼並不是 Iterator Methods 因為它沒有使用 yield return 回傳值也不是 IEnumerable<T>, 所以上面這種寫法是常見的傳統寫法,它會先建立一個暫時的 List<int> 保存中繼結果等處理完成後進行輸出。

假設我有一個 BindingList<int> 結構,我之後使用上面的方法產生的集合進行初始化,這會導致一個問題就是建構 BindingList 所用的 CreateSequence(100,0,5).ToList() 並不會複製一份新的結果而是複製地址,所以假設同時有另一個地方使用到了同一分資料地址那可能會導致 初始化錯誤。

var data = new BindingList<int>(CreateSequence(100,0,5).ToList());

另一個問題在做法 31 有提到,CreateSequence 因為不是 Iterator Methods 所以並不具備 Continuable methods 的特性,也就是說 它一但開始運行就必須完成為止。

當然解決的方法也很簡單那就是改成 Iterator Methods 就好。

static IEnumerable<int> CreateSequence(int numberOfElements, int startAt, int stepBy)
{
	for (var i = 0; i < numberOfElements; i++)
		yield return startAt + i * stepBy;
}

另外要提一下下面這兩種寫法,第一種回傳的是 IEnumerable<T> 所以建構 List<int> 時所用的建構函式使用的是 IEnumerable<T> 的多載版本, 第二種則是會先把呼叫 ToList() 方法把結果轉換成 List 這樣感覺起來效能比較差,但其實 BindingList 會直接使用剛剛建立的 List 的參考, 所以不會再完整複製一次,所以效能不會比較差。

var listStorage = new List<int>(CreateSequence(100, 0, 5));
var data = new BindingList<int>(CreateSequence(100,0,5).ToList());

最後又對 Continuable methods 這個特性提出一個範例,也就是本做法要提出的重點,下面這段程式碼展現出可中斷的特性, 雖然 CreateSequence 會建立一個 10000 個元素的集合,但是關鍵在最後的 FirstOrDefault 代表我們只需要第一個元素即可, 如果使用舊的寫法那會先把這 10000 個元素的集合先產生完成之後在挑出第一個。

但是在 yield return 的版本中透過中斷的特性,迴圈就真的只會執行一次,剩下的 9999 次都可以省略掉了。

void Main()
{
	var sequence = CreateSequence(10000, 0, 7).FirstOrDefault();
}

static IEnumerable<int> CreateSequence(int numberOfElements, int startAt, int stepBy)
{
	for (var i = 0; i < numberOfElements; i++)
		yield return startAt + i * stepBy;
}

Summary

這個做法將 Lazy Evaluation 特性進行解說,好處可以等待真正需要用到值的時候才會執行該方法, 基本上都是對過去提到的內容提出範例與補充說明,主要還是在建議使用者多使用 yield return