Effective C# (Covers C# 6.0), (includes Content Update Program): 50 Specific Ways to Improve Your C#, 3rd Edition By Bill Wagner 讀後心得
foreach 語法糖在開發 C# 程式很常被使用,其中實作了迭代器(interator)。本節提出盡可能回傳 IEnumerable 型別供外部使用與早期的檢查輸入合法性。
1. 利用迭代器的特性節省記憶體開銷。
public static IEnumerable<char> generateAlphabet( )
{
var letter = 'a';
while ( letter <= 'z' )
{
yield return letter;
letter++;
}
}
yield return 會回傳當前的值(此處為 char letter),並記錄當前位置;待外部 foreach loop 內處理完工作後繼續向下一個迭代運行。
此寫法將自動產生以下程式碼:
public class EmbeddedIterator : IEnumerable<char>
{
public static IEnumerable<char> generateAlphabet( ) =>
new EmbeddedIterator( );
public IEnumerator<char> GetEnumerator( ) =>
new LetterEnumerator( );
IEnumerator IEnumerable.GetEnumerator( ) =>
new LetterEnumerator( );
private class LetterEnumerator : IEnumerator<char>
{
private char _letter = ( char ) ( 'a' - 1 );
public char Current => _letter;
object IEnumerator.Current => _letter;
public void Dispose( )
{
// Elided.
}
public bool MoveNext( )
{
_letter++;
return _letter <= 'z';
}
public void Reset( ) =>
_letter = ( char ) ( 'a' - 1 );
}
}
Note:迭代器可以節省記憶體空間。在以上的範例中,不需事先將所有 char 存進記憶體中;而是在使用到時才取得值。但操作速度將不比預先快取或儲存在記憶體中快(yield return 利用 State Machine 記錄當前位置,造成額外開銷)。
2. 在建構階段就擲出例外,而非在第一次取得值或每一次迭代才檢查是否合法。
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 the letter 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 past z",
nameof( last ) );
var letter = first;
while ( letter <= last )
{
yield return letter;
letter++;
}
}
在這個方法中,外部需在第一次取得值時才能得知輸入的 first, last 是否合法。
相當於產生以下程式碼:
public class EmbeddedSubsetIterator : IEnumerable<char>
{
private readonly char _first;
private readonly char _last;
public EmbeddedSubsetIterator( char first, char last )
{
_first = first;
_last = last;
}
public static IEnumerable<char> generateAlphabetSubset(
char first, char last ) =>
new EmbeddedSubsetIterator( first, last );
public IEnumerator<char> GetEnumerator( ) =>
new LetterEnumerator( _first, _last );
IEnumerator IEnumerable.GetEnumerator( ) =>
new LetterEnumerator( _first, _last );
private class LetterEnumerator : IEnumerator<char>
{
private readonly char _first;
private readonly char _last;
private bool _isInitialized;
private char _letter = ( char ) ( 'a' - 1 );
public char Current => _letter;
object IEnumerator.Current => _letter;
public LetterEnumerator( char first, char last )
{
_first = first;
_last = last;
}
public void Dispose( )
{
// Elided.
}
public bool MoveNext( )
{
if ( !_isInitialized )
{
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 the letter 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 past z",
nameof( _last ) );
_letter = ( char ) ( _first - 1 );
_isInitialized = true;
}
_letter++;
return _letter <= _last;
}
public void Reset( ) => _isInitialized = false;
}
}
為了讓外部早期得知輸入範圍是否合法,且在產生 IEnumerable 物件以前就能檢查,需修改程式碼。
改寫程式碼:
public static IEnumerable<char> generateAlphabetSubset2(
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 the letter 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 past z",
nameof( last ) );
return generateAlphabetSubsetImp( first, last );
}
private static IEnumerable<char> generateAlphabetSubsetImp(
char first, char last )
{
var letter = first;
while ( letter <= last )
{
yield return letter;
letter++;
}
}
在建構 IEnumerable<char> 的同時就檢查輸入合法性,早期讓外部得知錯誤與否。
結論:
1. 盡量回傳 IEnumerable 供外部使用,若外部有快取或預先儲存結果的需求;外部自行呼叫 ToList 或 ToArray 即可滿足。
2. 在建構階段就檢查輸入合法性。
3. 回傳 IEnumerable 意味著回傳值無法被修改(若需修改集合需自行在記憶體中儲存另一個副本),確保了回傳值不可變性。
1. 盡量回傳 IEnumerable 供外部使用,若外部有快取或預先儲存結果的需求;外部自行呼叫 ToList 或 ToArray 即可滿足。
2. 在建構階段就檢查輸入合法性。
3. 回傳 IEnumerable 意味著回傳值無法被修改(若需修改集合需自行在記憶體中儲存另一個副本),確保了回傳值不可變性。
參考資料:
Iterator block implementation details: auto-generated state machines