Chapter 4 - Item 29 : Prefer Iterator Methods to Returning Collections

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 意味著回傳值無法被修改(若需修改集合需自行在記憶體中儲存另一個副本),確保了回傳值不可變性。

參考資料:
Iterator block implementation details: auto-generated state machines