在計算機這個范疇內存在許多種類的集合,從簡單的數據結構比如數組、鏈表,到復雜的數據結構比如紅黑樹,哈希表。盡管這些數據結構的內部實現和外部特征大相徑庭,但是遍歷集合的內容確是一個共同的需求。.NET Framework通過IEnumerable和IEnumerator接口實現遍歷集合功能。
Non-Generic | Generic | 備注 |
IEnumerator | IEnumerator<T> | |
IEnumerable | IEnumerable<T> | 僅可遍歷 |
ICollection | ICollection<T> | 遍歷,可統計集合元素 |
IDictionaryIList | IDictionary<TKey,TValue>IList<T> | 擁有更過的功能 |
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";// IEnumeratorIEnumerator rator = s.GetEnumerator();while (rator.MoveNext()) Console.Write(rator.Current + ".");Console.WriteLine();// IEnumerableforeach (char c in s) Console.Write(c + ".");
一般地,很少調用GetEnumerator方法得到IEnumerator接口,這是由于C#提供了foreach語法(foreach語法編譯后,會自動調用GetEnumerator從而遍歷集合),這使得代碼變得更簡潔。
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>
而實現IEnumerable/IEnumerable<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代碼中,我們可以看到,編譯器同樣生產了一個內部的
新聞熱點
疑難解答