C# 中如果需要使用關鍵字foreach去迭代(輪詢)一遍一個集合的內容,那麼就需要這個類實現IEnumerable接口。
C# 很多官方提供的Collection集合已經實現了IEnumerable接口的,比如ArrayList,Queue,Stack等類都實現了IEnumerable接口,我們可以放心使用foreach。(ArrayList是通過IList接口間接包含了IEnumerable接口,Queue和Stack則是通過擁有ICollection接口間接擁有IEnumerable接口)。
public interface IList : System.Collections.ICollection
public interface ICollection : System.Collections.IEnumerable
如果要自己實現一個擁有迭代器(也就是foreach功能)的類,那就可以包含IEnumerable接口。
IEnumerable接口僅僅需要實現一個GetEnumerator()方法:
public System.Collections.IEnumerator GetEnumerator ();
觀察這方法,需要返回一個叫做IEnumerator的接口,因此,一個類要想可迭代,還需要進一步實現IEnumerator類,這個纔是真正獲取到的迭代器,本文我們暫且稱這個類爲輔助類。
IEnumerator類要實現的方法和屬性會多一些,包括以下內容:
public object Current { get; }
public bool MoveNext ();
public void Reset ();
至此,要想實現一個擁有迭代器的類,我們可以(注意,是可以,不是必須)實現一個IEnumerable接口,這時還需要實現一個擁有IEnumerator接口的輔助類。
舉個例子(這個例子來自微軟):
using System;
using System.Collections;
// Simple business object.
public class Person
{
public Person(string fName, string lName)
{
this.firstName = fName;
this.lastName = lName;
}
public string firstName;
public string lastName;
}
// Collection of Person objects. This class
// implements IEnumerable so that it can be used
// with ForEach syntax.
public class People : IEnumerable
{
private Person[] _people;
public People(Person[] pArray)
{
_people = new Person[pArray.Length];
for (int i = 0; i < pArray.Length; i++)
{
_people[i] = pArray[i];
}
}
// Implementation for the GetEnumerator method.
IEnumerator IEnumerable.GetEnumerator()
{
return (IEnumerator) GetEnumerator();
}
public PeopleEnum GetEnumerator()
{
return new PeopleEnum(_people);
}
}
// When you implement IEnumerable, you must also implement IEnumerator.
public class PeopleEnum : IEnumerator
{
public Person[] _people;
// Enumerators are positioned before the first element
// until the first MoveNext() call.
int position = -1;
public PeopleEnum(Person[] list)
{
_people = list;
}
public bool MoveNext()
{
position++;
return (position < _people.Length);
}
public void Reset()
{
position = -1;
}
object IEnumerator.Current
{
get
{
return Current;
}
}
public Person Current
{
get
{
try
{
return _people[position];
}
catch (IndexOutOfRangeException)
{
throw new InvalidOperationException();
}
}
}
}
class App
{
static void Main()
{
Person[] peopleArray = new Person[3]
{
new Person("John", "Smith"),
new Person("Jim", "Johnson"),
new Person("Sue", "Rabon"),
};
People peopleList = new People(peopleArray);
// 下面的foreach這裏就是用到了迭代器。
foreach (Person p in peopleList)
Console.WriteLine(p.firstName + " " + p.lastName);
}
}
/* This code produces output similar to the following:
*
* John Smith
* Jim Johnson
* Sue Rabon
*
*/
這個例子中,我們看到實現了一個People類,main方法中可以對peopleList實施了foreach迭代。而之所以可以對peopleList進行迭代,就是因爲People類實現了IEnumerable接口。因爲需要實現IEnumerable接口,因此,也就實現了一個IEnumerator接口。
代碼裏,我們發現它有兩個Current,這個涉及到隱式接口實現和顯示接口實現,請參考文章C# 隱式實現接口和顯示實現接口。
仔細觀察這個PeopleEnum類,裏面有個Person數組。position相當於一個遊標(也就是數組的下標),遊標爲-1的時候表示在數組第一項之前,0表示數組的第一個項,以此類推。Current屬性表示當前迭代器所指向的這個項,當越界時返回一個異常。MoveNext()方法就是需要向前移動遊標,並且當遊標越界時需要返回false,否則返回true。Reset()就是把遊標初始化爲-1。當我們自己想實現一個類似的IEnumerator迭代器,那麼就可以完全類似拷貝這個例子去實現自己的迭代器。
這裏需要注意的是,如果一個類可以foreach,並不是必須實現IEnumerable接口,事實上,只要這個類有一個GetEnumerator()方法即可。由於GetEnumerator()方法需要有一個返回類,這個返回類需要擁有IEnumerator接口,因此還需要實現IEnumerator接口。事實上,這個返回類只需要實現Current, MoveNext()和Reset()方法即可。
下面更改下上面的例子(去掉IEnumerable和IEnumerator兩個接口名字):
using System;
using System.Collections;
// Simple business object.
public class Person
{
public Person(string fName, string lName)
{
this.firstName = fName;
this.lastName = lName;
}
public string firstName;
public string lastName;
}
// Collection of Person objects. This class
// implements IEnumerable so that it can be used
// with ForEach syntax.
public class People
{
private Person[] _people;
public People(Person[] pArray)
{
_people = new Person[pArray.Length];
for (int i = 0; i < pArray.Length; i++)
{
_people[i] = pArray[i];
}
}
// Implementation for the GetEnumerator method.
public PeopleEnum GetEnumerator()
{
return new PeopleEnum(_people);
}
}
// When you implement IEnumerable, you must also implement IEnumerator.
public class PeopleEnum
{
public Person[] _people;
// Enumerators are positioned before the first element
// until the first MoveNext() call.
int position = -1;
public PeopleEnum(Person[] list)
{
_people = list;
}
public bool MoveNext()
{
position++;
return (position < _people.Length);
}
public void Reset()
{
position = -1;
}
public Person Current
{
get
{
try
{
return _people[position];
}
catch (IndexOutOfRangeException)
{
throw new InvalidOperationException();
}
}
}
}
class App
{
static void Main()
{
Person[] peopleArray = new Person[3]
{
new Person("John", "Smith"),
new Person("Jim", "Johnson"),
new Person("Sue", "Rabon"),
};
People peopleList = new People(peopleArray);
foreach (Person p in peopleList)
Console.WriteLine(p.firstName + " " + p.lastName);
}
}
/* This code produces output similar to the following:
*
* John Smith
* Jim Johnson
* Sue Rabon
*
*/
這個例子仍然是可以正常運行的。
這說明一個類要想可以foreach迭代,不需要顯示實現IEnumerable接口和IEnumerator接口,只需要實現GetEnumerator()方法,實現一個GetEnumerator()方法返回輔助類,這個輔助類只需要實現Current, MoveNext()和Reset()方法即可。
細心地同學肯定會發現,代碼中根本沒有直接調用GetEnumerator(),Current, MoveNext()和Reset()啊。實際上這是被foreach隱藏掉了。VS Code裏,如果查看MoveNext()方法時,會發現它被引用了一次,如下圖所示:
同樣的,GetEnumerator(),Current也被foreach語句引用了。這是foreach底層機制調用的,所以我們看不到。