天天看點

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()方法時,會發現它被引用了一次,如下圖所示:

C# IEnumerable接口

同樣的,GetEnumerator(),Current也被foreach語句引用了。這是foreach底層機制調用的,是以我們看不到。