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底层机制调用的,所以我们看不到。