IEnumerable / IEnumerator
首先,IEnumerable / IEnumerator 接口定義如下:
public interface IEnumerable /// 可枚舉接口
{
IEnumerator GetEnumerator();
}
public interface IEnumerator /// 枚舉器接口
{
object Current { get; }
bool MoveNext();
void Reset();
}
注:Current 沒有 set 方法,在 foreach 中不能修改元素 var item 的值。
· IEnumerable:聲明式的接口,聲明實現了該接口的類是可枚舉類型;
· IEnumerator:實現式的接口,IEnumerator 對象說明如何實現一個枚舉器;
通過繼承 IEnumerable / IEnumerator 接口實現自定義類使用 foreach 語句來遍歷自身元素。邏輯關係圖:
People <-> MyClass 實現 IEnumerable 接口的 GetEnumerator()方法
EnumeratorPeople <-> MyEnumerator 實現 IEnumerator 接口
· 定義Person類
public class Person
{
private string name;
private int age;
public Person(string _name, int _age){
this.name = _name, this.age = _age;
}
public override string ToString(){
return string.Format("Name:{0},Age:{1}.", name, age);
}
}
數組定義見主函數,以下2種遍歷數組的方法等同,因爲所有數組的基類都是 System.Array ,System.Array 類實現了 IEnumerable 接口,可以直接通過 GetEnumerator() 方法返回枚舉數對象,枚舉數對象可以依次返回請求的數組的元素。
// 利用 foreach 遍歷數組
foreach (var person in persons)
Console.WriteLine(person.ToString());
// 利用 IEnumerable ~ IEnumerator 遍歷數組
IEnumerator it = persons.GetEnumerator();
while (it.MoveNext()){
Person obj = (Person)(it.Current); // 需強制類型轉換
Console.WriteLine(obj.ToString());
}
· 定義People類 (MyClass)
public class People
{
private Person[] persons;
public People(Person[] _persons){
persons = new Person[_persons.Length];
for (int i = 0; i < _persons.Length; ++i){
persons[i] = _persons[i];
}
}
}
注意,People 類的 persons 數組是 private 變量,在主測函數中是無法遍歷訪問的。
方法 1:將 private 更改爲 public:
foreach (var person in people.persons)
Console.WriteLine(person.ToString());
方法 2:People 類繼承 IEnumerable 接口並實現 GetEnumerator() 方法,有 2 種方法:
[-1-]. 利用數組默認實現了 IEnumerable 和 IEnumerator 接口的事實,重新定義 People 類:
public class People : IEnumerable
{
... ...
public IEnumerator GetEnumerator(){
return persons.GetEnumerator(); // 方法 1
}
}
[-2-]. 自定義枚舉數類 (EnumeratorPeople 類如下),繼承並實現 IEnumerator 接口,重新定義 People 類:
public class People : IEnumerable
{
... ...
public IEnumerator GetEnumerator(){
return new EnumeratorPeople(persons); // 方法 2
}
}
此時,在方法2中自定義類可以使用如下 foreach 語句來遍歷自身元素:
foreach (var person in people)
Console.WriteLine(person.ToString());
· 定義EnumeratorPeople類 (MyEnumerator)
public class EnumeratorPeople : IEnumerator
{
private int position = -1;
private Person[] persons;
public EnumeratorPeople(Person[] _persons){
persons = new Person[_persons.Length];
for (int i = 0; i < _persons.Length; ++i)
persons[i] = _persons[i];
}
public object Current{
get{ return persons[position]; }
}
public bool MoveNext(){
++position;
return (position < persons.Length);
}
public void Reset(){
position = -1;
}
}
· 主函數測試代碼
class Program{
static void Main(string[] args){
Person[] persons = {
new Person("abc",25), new Person("xyz",22),
new Person("qwer",12), new Person("pm",20) };
People people = new People(persons);
}
}
總結
一個類型是否支持foreach遍歷,本質上是實現 IEnumerator 接口,2 種方法:
(1)自定義類只要繼承 IEnumerable 接口並實現無參 GetEnumerator() 方法即可,最簡單;
(2)在(1)基礎上,定義 MyEnumerator 類繼承並實現 IEnumerator 接口;
擴展 :
foreach 語句隱式調用集合的無參 GetEnumerator() 方法。其實,不論集合是否有實現 IEnumerable 接口,只要必須提供無參 GetEnumerator() 方法並返回包含 Current 屬性和 MoveNext() 方法的 IEnumerator 對象即可,然後編譯器會自動去綁定,無需依賴 IEnumerable 和 IEnumerator 接口,從而實現 foreach 對自定義集合類的遍歷。
public class People
{
public class EnumeratorPeople
{
private int position = -1;
private Person[] enumPersons;
public EnumeratorPeople(Person[] _persons){
enumPersons = new Person[_persons.Length];
for (int i = 0; i < _persons.Length; ++i){
enumPersons[i] = _persons[i];
}
}
public object Current{
get { return enumPersons[position]; }
}
public bool MoveNext(){
++position;
return ( position < enumPersons.Length );
}
public void Reset(){
position = -1;
}
}
private Person[] persons;
public People(Person[] _persons){
persons = new Person[_persons.Length];
for (int i = 0; i < _persons.Length; ++i){
persons[i] = _persons[i];
}
}
public EnumeratorPeople GetEnumerator(){
return new EnumeratorPeople(persons);
}
}
此處,枚舉數類聲明爲嵌套類,或者集成爲一個類,也可以分成單獨的 2 個類均可。
延伸問題
· for 與 foreach
for 先取全部再遍歷,foreach 邊遍歷邊取值;
· Linq to Object 中返回 IEnumerable 類型?
IEnumerable 是延遲加載的。
參考
· 傳統遍歷與迭代器;
· IEnumerable和IEnumerator 詳解;
· 自定義類實現foreach;深入理解 foreach;
IEnumerable<T> / IEnumerator<T>
優缺點對比
· 非泛型:非類型安全,返回object類型的引用、需要再轉化爲實際類型 (值類型需要裝箱和拆箱);
· 泛型:類型安全,直接返回實際類型的引用;
首先,IEnumerable<T> / IEnumerator<T> 接口定義如下:
public interface IEnumerable<out T> : IEnumerable
{
IEnumerator<T> GetEnumerator();
}
public interface IEnumerator<out T> : IEnumerator, IDisposable
{
T Current { get; }
}
其中,接口 IDisposable 定義爲:
public interface IDisposable{
void Dispose();
}
邏輯關係圖:
People <-> MyClass 實現 IEnumerable<T> 接口的 泛型GetEnumerator()方法
GenEnumPeople <-> MyGenEnumerator 實現 IEnumerator<T> 接口
注:顯式實現非泛型版本,在類中實現泛型版本!如下,類 MyGenEnumerator 實現了 IEnumerator<T>,類MyClass 實現了 IEnumerable<T> 接口。
public class MyClass : IEnumerable<T>
{
public IEnumerator<T> GetEnumerator() { } // IEnumerable<T> 版本
IEnumerator IEnumerable.GetEnumerator() { } // IEnumerable 版本
}
public class MyGenEnumerator : IEnumerator<T>
{
public T Current { get; } // IEnumerator<T> 版本
public bool MoveNext() { }
public void Reset() { }
object IEnumerator.Current { get; } // IEnumerator 版本
public void Dispose() { }
}
· 定義 People 類 (MyClass)
public class People : IEnumerable<Person>
{
private Person[] persons;
public People(Person[] _persons){
persons = new Person[_persons.Length];
for (int i = 0; i < _persons.Length; ++i)
persons[i] = _persons[i];
}
public IEnumerator<Person> GetEnumerator(){
return new GenericEnumeratorPeople(persons);
}
IEnumerator IEnumerable.GetEnumerator(){ // 顯式實現
return this.GetEnumerator();
}
}
· 定義 GenEnumPeople 類 (MyGenEnumerator)
public class GenEnumPeople : IEnumerator<Person>
{
private int position = -1;
private Person[] persons;
public GenericEnumeratorPeople(Person[] _persons){
persons = new Person[_persons.Length];
for (int i = 0; i < _persons.Length; ++i)
persons[i] = _persons[i];
}
public Person Current{
get { return persons[position]; }
}
object IEnumerator.Current{ // 顯式實現
get { return this.Current; }
}
public bool MoveNext(){
++position;
return (position < persons.Length);
}
public void Reset() { position = -1; }
public void Dispose() { Console.WriteLine("void Dispose()"); }
}
其中,IDisposable 接口的學習參見 由 IDisposable 到 GC;
擴展 :泛型委託應用;
迭代器
C#2.0 利用迭代器可以簡單實現 GetEnumerator() 函數。迭代器是用於返回相同類型的值的有序集合的一段代碼。利用 yield 關鍵字,實現控制權的傳遞和循環變量的暫存,使類或結構支持 foreach 迭代,而不必顯式實現 IEnumerable 或 IEnumerator 接口,由 JIT 編譯器輔助編譯成實現了 IEnumerable 或 IEnumerator 接口的對象。yield return 提供了迭代器一個重要功能,即取到一個數據後馬上返回該數據,不需要全部數據加載完畢,有效提高遍歷效率(延遲加載)。
- yield 關鍵字用於指定返回的值,yield return 語句依次返回每個元素,yield break 語句終止迭代;
- 到達 yield return 語句時,保存當前位置,下次調用迭代器時直接從當前位置繼續執行;
- 迭代器可以用作方法、運算符或get訪問器的主體實現,yield 語句不能出現在匿名方法中;
- 迭代器返回類型必須爲 IEnumerable、IEnumerator、IEnumerable<T> 或 IEnumerator<T>;
邏輯關係圖:
· · 返回 IEnumerator 類型
返回此類型的迭代器通常爲默認迭代器。
· People 類
public class People
{
private Person[] persons;
public People(Person[] _persons){
persons = new Person[_persons.Length];
for (int i = 0; i < _persons.Length; ++i)
persons[i] = _persons[i];
}
public IEnumerator GetEnumerator(){
return IterMethod1; // [1]
return IterMethod2(); // [2]
}
public IEnumerator IterMethod1 // [1].屬性返回 IEnumerator
{
get {
for (int i = 0; i < persons.Length; ++i)
yield return persons[i];
}
}
public IEnumerator IterMethod2() // [2].函數返回 IEnumerator (推薦)
{
for (int i = 0; i < persons.Length; ++i)
yield return persons[i];
}
}
· 主函數測試代碼
People people = new People(persons);
foreach (var person in people)
Console.WriteLine(person.ToString());
· · 返回 IEnumerable 類型
返回此類型的迭代器通常用於實現自定義迭代器。
· People 類
public class People
{
private Person[] persons;
public People(Person[] _persons){
persons = new Person[_persons.Length];
for (int i = 0; i < _persons.Length; ++i)
persons[i] = _persons[i];
}
public IEnumerator GetEnumerator(){
return IterMethod1.GetEnumerator(); // [1]
return IterMethod2().GetEnumerator(); // [2]
}
public IEnumerable IterMethod1 // [1].自定義迭代器 1
{
get{
for (int i = 0; i < persons.Length; ++i)
yield return persons[i];
}
}
public IEnumerable IterMethod2() // [2].自定義迭代器 2 (推薦)
{
for (int i = 0; i < persons.Length; ++i)
yield return persons[i];
}
}
· 主函數測試代碼
People people = new People(persons);
foreach (var person in people) // 默認
Console.WriteLine(person.ToString());
foreach (var person in people.IterMethod1) // [1]
Console.WriteLine(person.ToString());
foreach (var person in people.IterMethod2()) // [2]
Console.WriteLine(person.ToString());
對於返回泛型IEnumerator<T>、IEnumerable<T>的迭代器,同理。