2013年7月26日 星期五

IEnumerable/IEnumerator 的兩三事


以下是一個傳統且結實的 Enumerable & Enumerator 範例:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;

namespace ConsoleApplication1 {
    class Program {
        class MyEnumerable : IEnumerable <int >, IEnumerable {
            private int[] values;
            private int offset;
            public MyEnumerable( int[] values, int offset) {
                this.values = values;
                this.offset = offset;
            }
            public IEnumerator<int > GetEnumerator() {
                return new MyIterator(this );
            }
            IEnumerator IEnumerable .GetEnumerator() {
                return this.GetEnumerator();
            }
            private class MyIterator : IEnumerator <int >, IEnumerator {
                private MyEnumerable target;
                private int pos;
                public MyIterator( MyEnumerable target) {
                    this.target = target;
                    this.pos = -1;
                }
                public bool MoveNext() {
                    if( this.pos != this.target.values.Length) this.pos++;
                    return this.pos < this.target.values.Length;
                }
                public int Current {
                    get {
                        if( this.pos == -1 || this.pos == this.target.values.Length) {
                            throw new InvalidOperationException();
                        }
                        int index = this.pos + this.target.offset;
                        index = index % this.target.values.Length;
                        return this.target.values[index];
                    }
                }
                object IEnumerator.Current {
                    get {
                        return this.Current;
                    }
                }
                public void Reset() { this.pos = -1; }
                void IEnumerator.Reset() { this .Reset(); }
                public void Dispose() { }
            }
        }
        static void Main( string[] args) {
            MyEnumerable list = new MyEnumerable (new int [] {1,2,3,4,5,6}, 2);
            foreach( int i in list) Console.WriteLine(i);
            Console.WriteLine( "-------------------------");
            foreach( object o in (( IEnumerable)list)) Console .WriteLine(o);
            Console.WriteLine( "-------------------------");
            IEnumerator<int > iterator = list.GetEnumerator();
            while(iterator.MoveNext()) Console.WriteLine(iterator.Current.ToString());
            (iterator as IDisposable).Dispose();
            Console.ReadKey();
        }
    }
}

自從 C# 2.0 之後, 上面的程式可以被簡化成下面這樣

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;

namespace ConsoleApplication1 {
    class Program {
        class MyEnumerable : IEnumerable <int >, IEnumerable {
            private int[] values;
            private int offset;
            public MyEnumerable( int[] values, int offset) {
                this.values = values;
                this.offset = offset;
            }
            public IEnumerator<int > GetEnumerator() {
                for( int index = 0; index < values.Length; index++) {
                    yield return values[(index + this.offset) % this.values.Length];
                }
            }
            IEnumerator IEnumerable .GetEnumerator() {
                return this.GetEnumerator();
            }
        }
        static void Main( string[] args) {
            MyEnumerable list = new MyEnumerable (new int [] {1,2,3,4,5,6}, 2);
            foreach( int i in list) Console.WriteLine(i);
            Console.WriteLine( "-------------------------");
            foreach( object o in (( IEnumerable)list)) Console .WriteLine(o);
            Console.WriteLine( "-------------------------");
            IEnumerator<int > iterator = list.GetEnumerator();
            while(iterator.MoveNext()) Console.WriteLine(iterator.Current.ToString());
            (iterator as IDisposable).Dispose();
            Console.ReadKey();
        }
    }
}

實際上發生的事情是: 
當 Compiler 看到 yield return 時,
它會自動地幫我們產生一個 Enumerator class,
就像我們之前自己寫的那樣!!!
不過, 有幾個注意事項, 首先觀察下面程式碼的輸出:

using System;
using System.Collections.Generic;

namespace ConsoleApplication1 {
    class Program {
        private static readonly String padding = new String( ' ', 30);
        private static IEnumerable<int > CreateEnumerable() {
            Console.WriteLine( "{0}Start of CreateEnumerable()" , padding);
            for( int i = 0; i < 3; i++) {
                Console.WriteLine( "{0}About to yield {1}", padding, i);
                yield return i;
                Console.WriteLine( "{0}After yield", padding);
            }
            Console.WriteLine( "{0}End of CreateEnumerable()", padding);
        }
        static void Main( string[] args) {
            IEnumerable<int > iterable = CreateEnumerable();
            IEnumerator<int > iterator = iterable.GetEnumerator();
            Console.WriteLine( "Starting to iterate");
            while( true) {
                Console.WriteLine( "Calling MoveNext()...");
                bool result = iterator.MoveNext();
                Console.WriteLine( "... MoveNext result={0}", result);
                if(!result)
                    break;
                Console.WriteLine( "Fetching Current...");
                Console.WriteLine( "... Current result={0}", iterator.Current);
            }
            Console.ReadKey();
        }
    }
}

因為 Compiler 動了很大的手腳, 
所以上面那隻程式的輸出會讓人很意外!!!
有興趣的話可以使用 Reflector 或 ILDasm 來觀察 Compiler 究竟做了甚麼事.

使用 yield return 來產生 Enumerator/Enumerable 時,
有幾個特別的事項要提醒:
1. code 會在第一次 MoveNext() 時才被執行
2. code 裏頭可以有 try 以及 finally 區塊, 但不可以有 catch 區塊
3. finally 區塊實際上會被移至 Enumerator 的 Dispose() 裡
4. 在第一次 MoveNext() 被呼叫前, Current 會是 default(T)
5. 當 MoveNext() 回傳 false 後, Current 永遠會是最後值
6. 呼叫 Reset() 會丟例外

出處: C# In Depth 2Ed Chapter 7

沒有留言:

張貼留言