在計算機這個范疇內存在許多種類的集合,從簡單的數據結構比如數組、鏈表,到復雜的數據結構比如紅黑樹,哈希表。盡管這些數據結構的內部實現和外部特征大相徑庭,但是遍歷集合的內容確是一個共同的需求。.NET Framework通過IEnumerable和IEnumerator接口實現遍歷集合功能。
Non-Generic | Generic | 備注 |
IEnumerator | IEnumerator<T> | |
IEnumerable | IEnumerable<T> | 僅可遍歷 |
ICollection | ICollection<T> | 遍歷,可統計集合元素 |
IDictionary IList |
IDictionary<TKey,TValue> IList<T> |
擁有更過的功能 |
IEnumerable與IEnumerator
IEnumerator接口定義了遍歷協議--在這個協議中,集合中的元素使用向前的方式進行遍歷。它的聲明如下:
public interface IEnumerator { bool MoveNext(); Object Current { get; } void Reset(); }
MoveNext將當前元素或指針移動到下一個位置,如果下一個位置沒有元素那么返回false。Current返回在當前值位置的元素。在獲取集合的第一個元素之前,必須調用MoveNext方法--這對於空集合同樣適用。Reset方法,這移動到初始位置,從而允許集合可以再次遍歷。Reset更過多是為COM互操作而設計:應該盡量直接避免調用此方法,因為它並沒有得到普遍的支持(直接調用此方法是不必要的,因為創建一個新的列舉實例更容易)。
集合一般都不實現列舉器,相反,它們通過IEnurable接口提供列舉器
public interface IEnumerable { IEnumerator GetEnumerator(); }
通過定義一個單一返回列舉器的方法,IEnumerable接口提供了更多的靈活性,從而各個實現類的遍歷集合的邏輯可以各部相同。這也就意味着每個集合的使用者都可以創建自己的方法遍歷集合而不會相互影響。IEnumerable可以被視作IEnumeratorProvider,它是所有集合類都必須實現的一個接口。
下面的代碼演示了如何使用IEnumerable和IEnumerator:
string s = "Hello"; // IEnumerator IEnumerator rator = s.GetEnumerator(); while (rator.MoveNext()) Console.Write(rator.Current + "."); Console.WriteLine(); // IEnumerable foreach (char c in s) Console.Write(c + ".");
一般地,很少調用GetEnumerator方法得到IEnumerator接口,這是由於C#提供了foreach語法(foreach語法編譯后,會自動調用GetEnumerator從而遍歷集合),這使得代碼變得更簡潔。
IEnumerable<T>與IEnumerator<T>
IEnumerator和IEnumerable對應的Generic接口定義如下:
public interface IEnumerator<out T> : IDisposable, IEnumerator { new T Current { get; } }
public interface IEnumerable<out T> : IEnumerable { new IEnumerator<T> GetEnumerator(); }
Generic的Current和GetEnumerator,增加了接口IEnumerable<T>與IEnumerator<T>的類型安全性,避免了對值類型進行裝箱操作,對於集合的使用者更加便利。請注意,數字類型默認實現了IEnumerable<T>接口。
正是由於實現了類型安全的接口,方法Test2(arr)在編譯時就會報錯:
static void Main(string[] args) { char[] arr = new char[] { '1', '2', '3' }; Test1(arr); // ok Test2(arr); // complie-error: cannot convert from char[] to IEnumerable[] Console.ReadLine(); } static void Test1(IEnumerable numbers) { foreach (object i in numbers) Console.Write(i + ","); } static void Test2(IEnumerable<int> numbers) { foreach (object i in numbers) Console.Write(i + ","); }
請注意,Array默認實現了IEnumerable<T>接口,那么它同時必然實現了IEnumerable接口。雖然char[]不能轉換成IEnumrable<int>,但是卻可以轉換成IEnumeable,所以Test1可以通過編譯,而Test2不能通過編譯(類型轉化失敗錯誤)
對於集合類,對外暴露IEnumerable<T>是標准做法;並需要顯示地實現IEnumerable接口,從而隱藏非Generic的IEnumerable。此時,你再調用GetEnumerator,將得到IEnumerator<T>。但有時候,為了兼容非Generic的集合,我們可以不遵守這個規則。最好的例子就是數組集合,數組必須返回非generic的IEnumerator以避免與早期的代碼沖突。在這種情況下,為了獲取IEnumerator<T>,就必須先把數組顯示地轉化為Generic接口,然后再獲取:
char[] arr = new char[] { '1', '2', '3' }; var rator = ((IEnumerable<char>)arr).GetEnumerator();
幸運的是,你很少需要編寫這樣的代碼,這就要歸功於foreach語句。
IEnumerable<T>和IDisposable
IEnumerator<T>繼承了IDisposable。這就允許列舉器可以擁有資源的引用比如數據庫連接,從而確保在遍歷完成后釋放這些資源。foreach會語句會識別這個特性,比如,下面的foreach語句
IList<char> chars =new List<char>(){'a', 'b', 'c'}; foreach (char c in chars) Console.Write(c);
編譯后的代碼為:
.method private hidebysig static void Main(string[] args) cil managed { ...... IL_0026: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> class [mscorlib]System.Collections.Generic.IEnumerable`1<char>::GetEnumerator() IL_002b: stloc.3 .try { ....... System.Collections.Generic.IEnumerator`1<char>::get_Current() ...... IL_0036: call void [mscorlib]System.Console::Write(char) ...... IL_003d: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext() ...... } // end .try finally { ...... IL_0055: callvirt instance void [mscorlib]System.IDisposable::Dispose() ...... } // end handler ...... } // end of method Program::Main
因此,如果實現了IEnumable<T>接口,執行foreach時,會轉化成調用GetEnumerator<T>, 在遍歷完成之后,釋放IEnumerator<T>。
實現列舉接口
當滿足下面的一個或多個條件時,需要實現IEnumerable或IEnumerable<T>
- 為了支持foreach語句
- 為了實現除了標准集合之外的集合都是可互操作的
- 為了滿足一個復雜集合接口
- 為了支持集合初始化
而實現IEnumerable/IEnumerable<T>,你必須提供一個列舉器,你可以通過下面三種方式實現
- 如果類包含了另外集合,那么需要返回所包含集合的列舉器
- 在迭遍歷內部使用yield return
- 實例化IEnumerator/IEnumerator<T>的實現
1)實例IEnumerator/IEnumerator<T>
返回另外一個集合的列舉器就是調用內部集合的GetEnumerator。但是,這只發生在簡單的場景中,在這樣的場景中,內部集合中的元素已經滿足需要。另外一種更為靈活的方式是通過yield return語句生成一個迭代器。迭代器(iteraotr)是C#語言特性,該特性用於輔助生產集合,同樣地foreach可與用於iterator以遍歷集合。一個迭代器自動處理IEnumerable和IEnumerator的實現。下面是一個簡單的例子
internal class MyCollection : IEnumerable { int[] data ={ 1, 2, 3 }; public IEnumerator GetEnumerator() { foreach (int i in data) yield return i; } }
請注意,GetEnumerator根本就沒有返回一個列舉器。依賴於解析yield return后的語句,編譯器編寫了一個隱藏的內嵌列舉器類,然后重構 GetEnumerator實現實例化,最后返回該類。迭代不僅功能強大而且簡單。
通過IL代碼,我們可以看到確實生產了一個內嵌的列舉器類
我們在上面代碼的基礎上,對MyCollecton做些許修改,使其不僅僅實現IEnumerable,還實現IEnumerable<T>
internal class MyCollection : IEnumerable<int> { int[] data ={ 1, 2, 3 }; public IEnumerator<int> GetEnumerator() { foreach (int i in data) yield return i; } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } }
因為IEnumerable<T>繼承了IEnumerable,因此我們必須實現generic的GetEnumerator和非generic的GetEnumerator。按照標准的做法,我們已經實現了Generic的GetEnumerator。因此對於非Generic的GetEnumerator,我們直接調用Generic的GetEnumerator即可,這是因為IEnumerable<T>繼承了IEnumerbale。
對應的IL代碼如下:(請注意編譯器實現的IEnumerator<Int32>接口,而不再是IEnumerator<Object>接口)
2)在使用yield return返回IEnumerable<T>
我們創建的類MyCollection可以做為復雜集合類的基本實現。但是,如果你不需要實現IEnumerable<T>,那么應可以通過yield return語句實現一個IEnumerable<T>,而不是編寫MyCollection這樣的類。也就是說你可以把迭代邏輯遷移到一個返回IEnumerable<T>的方法中,然后讓編譯器來為你完成剩余的事情。
class Program { static void Main(string[] args) { foreach(int i in GetSomeIntegers()) Console.WriteLine(i); Console.ReadLine(); } static IEnumerable<int> GetSomeIntegers() { int[] data = { 1, 2, 3 }; foreach (int i in data) yield return i; } }
與之對應的IL代碼
從IL代碼中,我們可以看到,編譯器同樣生產了一個內部的類,該類實現了IEnumerator<Int32>接口。
3)如果類包含了另外集合,那么需要返回所包含集合的列舉器
最后一種實現方式將就是編寫一個類直接實現IEnumerator接口。其實這也就是編譯器之前做的事情。在實際中,你不需要這么做。
首先我們來實現非Generic的IEnumerator
internal class MyCollection : IEnumerable { int[] data ={ 1, 2, 3 }; public IEnumerator GetEnumerator() { return new Enumerator(this); } private class Enumerator : IEnumerator { MyCollection collection; int index; public Enumerator(MyCollection collection) { this.collection = collection; index = -1; } public object Current { get { return collection.data[index]; } } public bool MoveNext() { if (index < collection.data.Length-1) { index++; return true; } return false; } public void Reset() { index = -1; } } }
然后,我們在上述代碼的基礎上,實現Generic的IEnumerator
internal class MyCollection : IEnumerable<Int32> { int[] data = { 1, 2, 3 }; // implement IEnumerable<T> public IEnumerator<Int32> GetEnumerator() { return new Enumerator(this); } // implement IEnumerable IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } private class Enumerator : IEnumerator<Int32> { MyCollection collection; int index; public Enumerator(MyCollection collection) { this.collection = collection; index = -1; } #region implement IEnumerator<T> public int Current { get { return collection.data[index]; } } public void Dispose() { } public bool MoveNext() { if (index < collection.data.Length - 1) { index++; return true; } return false; } public void Reset() { index = -1; } #endregion // implement IEnumerator object IEnumerator.Current { get { return Current; } } } }
Generic版本的IEnumerator比非Generic的IEnumberator效率高一些,因為不需要把int轉化成object,從而減少了裝箱的開銷。我們多看一眼此時對應的IL代碼:
顯然地,我們可以看到我們手動創建Enumerator與編譯器生成的Enumerator是一樣的
此外,當我們使用第二種方式的時候,如果我們有多個IEnumerable<T>的方法,那么編譯器會產生多個實現了IEnumerator<T>的類
class Program { static void Main(string[] args) { foreach (int i in GetSomeIntegers()) Console.WriteLine(i); foreach (int i in GetSomeOdds()) Console.WriteLine(i); Console.ReadLine(); } static IEnumerable<Int32> GetSomeIntegers() { int[] collection = { 1, 2, 3, 4, 5 }; foreach (int i in collection) yield return i; } static IEnumerable<Int32> GetSomeOdds() { int[] collection = { 1, 2, 3, 4, 5 }; foreach (int i in collection) if(i%2==1) yield return i; } }
對應的IL代碼可以看到有兩個內部IEnumerator<T>類
而下面的代碼只會產生一個IEnumerator<T>類
class Program { static void Main(string[] args) { foreach (int i in GetSomeIntegers()) Console.WriteLine(i); foreach (int i in GetSomeOdds()) Console.WriteLine(i); Console.ReadLine(); } static IEnumerable<Int32> GetSomeIntegers() { return GetDetails(); } static IEnumerable<Int32> GetSomeOdds() { return GetDetails(true); } private static IEnumerable<Int32> GetDetails(bool isOdd = false) { int[] collection = { 1, 2, 3, 4, 5 }; int index = 0; foreach (int i in collection) { if (isOdd && i % 2 == 1) yield return i; if (!isOdd) yield return collection[index]; index++; } } }
同樣地,下面的代碼也只會產生一個IEnumerator<T>類
.... static IEnumerable<Int32> GetSomeIntegers() { foreach (int i in GetDetails()) yield return i; } static IEnumerable<Int32> GetSomeOdds() { foreach (int i in GetDetails(true)) yield return i; } ....
由此,我們可以發現,在實現IEnumerable時,特別是有多個實現時,需要注意盡量減少編譯器生成IEnumerator的類的個數。我猜測在內部,編譯器應該是根據真正不同的yield return對於的iterator來確定IEnumerator類的個數。在我的示例代碼中,產出兩個IEnumerator類時,GetSomeIntegers和GetSomeOdds的yield return的iterator是不同的;而在產生一個IEnumerator類時,它們都指向GetDetails的yield return對應的iterator。
最后,我們再來看看IEnumerator與Iterator
在網上,並沒有關於兩者的明確區分,或許是我把兩個不該混淆的概念混淆了。下面是我自己的看法,如果不正確,歡迎指正:
1) 實現IEnumerator用於實現IEnumerable,與GetEnumerator方法關聯在一起,從而可以使用foreach;而且一旦一個類中確定了遍歷(MoveNext)的方式之后,那么就只有這一種方式去遍歷集合了。.NET Framework中大多數集合的IEnumerator都默認向前只讀的方式遍歷集合。
2)Iterator用於遍歷集合,可以有多個實現方式,唯一的要求是返回IEnumerator<T>,從某種意義上說,Iterator就是IEnumerator。兩者的區別是,前者一旦確定,就只能使用這個方式遍歷集合然后返回一個IEnumerator;而后者可以在多個方法中以多種方式遍歷集合然后返回不同的IEnumerator。(我認為,兩者的差別與IComparable和IComparer的差別類似)。