Effective C# 29.偏好以 Iterator 方法回傳集合 Effective C# 29.偏好以 Iterator 方法回傳集合 (Prefer Iterator Methods to Returning Collections)

Published on Monday, October 21, 2024

本做法提到了 Iterator Methodsyield return 這兩個新的名詞以及使用它的好處。 根據定義只要使用 yield return 並且回傳值是 IEnumerableIEnumerable<T> 就可以稱為這是一個 Iterator Methods

首先我們先看一下不使用 yield return 的產生英文26個字母的方法,這裡建立了一個 List<char> 來保存結果並輸出。

public static List<char> GenerateAlphabet()
{
	var result = new List<char>();
	var letter = 'a';
	while (letter <= 'z')
	{
		result.Add(letter);
		letter++;
	}
	
	return result;
}

接下來將同樣的方法改成使用 yield return 來處理,可以很明顯地發現 List<char> 被省略掉了,然後改成在迴圈中搭配 yield return 回傳結果。

public static IEnumerable<char> GenerateAlphabet()
{
	var letter = 'a';
	while (letter <= 'z')
	{
		yield return letter;
		letter++;
	}
}

雖然看起來沒什麼區別但是編譯過後的程式碼卻差非常的多,其中最重要的特性就是 Lazy Evaluation 也就是確定要使用才會去產生 IEnumerable<char> 內部的值。 這個特性在大型的資料集中是相當關鍵的,例如下面這個 allNumbers 變數如果馬上就把所有的值保存到記憶體中是非常浪費的。

var allNumbers = Enumerable.Range(0, int.MaxValue);

但是需要注意這個 Lazy Evaluation 特性最好不要把參數檢查放在 Iterator Methods 裡面,否則只有到真正需要用到值的時候才會進行參數檢查, 例如下面這段程式碼明顯的有錯誤,但是呼叫的當下並不會拋出錯誤而是會繼續運行,直到需要值的時候才會拋出錯誤。

void Main()
{
 	var result = GenerateAlphabetSubset('z', 'a');
}

public static IEnumerable<char> GenerateAlphabetSubset(char first, char last)
{
	if (first < 'a')
		throw new ArgumentException("first must be at least the letter a", nameof(first));
	if (first > 'z')
		throw new ArgumentException("first must be no greater than z", nameof(first));
	if (last < first)
		throw new ArgumentException("last must be at least as large as first", nameof(last));
	if (last > 'z')
		throw new ArgumentException("last must not be past z", nameof(last));
	var letter = first;
	while (letter <= last)
	{
		yield return letter;
		letter++;
	}
}

這個問題也很好解決,只需要把 Iterator Methods 獨立到另一個方法即可,將同一段邏輯分成兩部分這樣不僅馬上就會進行參數檢查也能同時保有 Lazy Evaluation 的特性。

public static IEnumerable<char> GenerateAlphabetSubset(char first, char last)
{
	if (first < 'a')
		throw new ArgumentException("first must be at least the letter a", nameof(first));
	if (first > 'z')
		throw new ArgumentException("first must be no greater than z", nameof(first));
	if (last < first)
		throw new ArgumentException("last must be at least as large as first", nameof(last));
	if (last > 'z')
		throw new ArgumentException("last must not be past z", nameof(last));
	return GenerateAlphabetSubsetImpl(first, last);
}
private static IEnumerable<char> GenerateAlphabetSubsetImpl(char first, char last)
{
	var letter = first;
	while (letter <= last)
	{
		yield return letter;
		letter++;
	}
}

Summary

這個做法主要在討論 Iterator Methodsyield return 使用上的好處,最大的好處就是節省記憶體也能同時讓產生集合的這個步驟快上不少, 但是要注意參數檢查這種需要事先執行的邏輯最好要隔離開來。