C# IEnumerable接口

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章