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底層機制調用的,是以我們看不到。