引子:公車售票員#

公車上人潮擁擠,售票員要求每位乘客買票:

  • 帶大件行李的乘客 → 行李也要補票
  • 不會中文的老外 → 售票員擠出英文「Please buy ticket」
  • 公車公司的同事 → 工作證不算乘車憑證,照樣買票
  • 抓到的小偷 → 「想走?可以,先買票再說!」

售票員不管乘客是誰、是人是物、是內部員工還是小偷,只要在車上就要買票——他逐一遍歷整車人,不放過任何一位。

這就是迭代器模式的生活原型。

迭代器模式#

迭代器模式(Iterator Pattern):提供一種方法順序訪問一個聚合物件中各個元素,而又不暴露該物件的內部表示。[DP]

適用情境:

  • 需要訪問一個聚合物件,不論這些物件是什麼都需要遍歷
  • 需要對聚合多種方式遍歷(從車頭到車尾、或從車尾到車頭)
  • 為遍歷不同的聚合結構提供統一的介面:開始、下一個、是否結束、當前項

Martin Fowler 甚至在其網站上提議撤銷此模式——因為現在的高級語言(C#、Java)已經把迭代器內建到語言裡了。

不過學習它的歷史與設計思想,仍有價值。

結構#

  • Iterator(迭代抽象類):定義 First()Next()IsDone()CurrentItem() 等抽象方法,統一介面
  • Aggregate(聚合抽象類):宣告 CreateIterator() 來建立迭代器
  • ConcreteIterator:繼承 Iterator,實作具體迭代邏輯
  • ConcreteAggregate:繼承 Aggregate,回傳對應的具體迭代器
classDiagram
    class Aggregate {
        <<abstract>>
        +CreateIterator() Iterator
    }
    class ConcreteAggregate
    class Iterator {
        <<abstract>>
        +First()
        +Next()
        +IsDone() bool
        +CurrentItem()
    }
    class ConcreteIterator
    Aggregate <|-- ConcreteAggregate
    Iterator <|-- ConcreteIterator
    ConcreteAggregate ..> ConcreteIterator : creates
    ConcreteIterator ..> ConcreteAggregate
abstract class Iterator
{
    public abstract object First();
    public abstract object Next();
    public abstract bool IsDone();
    public abstract object CurrentItem();
}

abstract class Aggregate
{
    public abstract Iterator CreateIterator();
}

class ConcreteIterator : Iterator
{
    private ConcreteAggregate aggregate;
    private int current = 0;
    public ConcreteIterator(ConcreteAggregate aggregate) { this.aggregate = aggregate; }

    public override object First()       => aggregate[0];
    public override object Next()
    {
        current++;
        return current < aggregate.Count ? aggregate[current] : null;
    }
    public override bool IsDone()         => current >= aggregate.Count;
    public override object CurrentItem()  => aggregate[current];
}

class ConcreteAggregate : Aggregate
{
    private IList<object> items = new List<object>();
    public override Iterator CreateIterator() => new ConcreteIterator(this);
    public int Count => items.Count;
    public object this[int index]
    {
        get => items[index];
        set => items.Insert(index, value);
    }
}

客戶端:

ConcreteAggregate a = new ConcreteAggregate();
a[0] = "大鳥";
a[1] = "小菜";
a[2] = "行李";
a[3] = "老外";
a[4] = "公車內部員工";
a[5] = "小偷";

Iterator i = new ConcreteIterator(a);
object item = i.First();
while (!i.IsDone())
{
    Console.WriteLine($"{i.CurrentItem()} 請買車票!");
    i.Next();
}

多種遍歷方式#

迭代器抽象的價值在於支援多種遍歷方式——售票員可以從車頭走到車尾,也可以從車尾走到車頭:

class ConcreteIteratorDesc : Iterator
{
    private ConcreteAggregate aggregate;
    private int current = 0;
    public ConcreteIteratorDesc(ConcreteAggregate aggregate)
    {
        this.aggregate = aggregate;
        current = aggregate.Count - 1;
    }

    public override object First()  => aggregate[aggregate.Count - 1];
    public override object Next()
    {
        current--;
        return current >= 0 ? aggregate[current] : null;
    }
    public override bool IsDone()    => current < 0;
    public override object CurrentItem() => aggregate[current];
}

客戶端只要把 new ConcreteIterator(a) 換成 new ConcreteIteratorDesc(a) 即可。

.NET 的迭代器實作#

.NET 已將迭代器整合到語言層級,提供 IEnumerableIEnumerator 介面。

public interface IEnumerator
{
    object Current { get; }
    bool MoveNext();
    void Reset();
}

public interface IEnumerable
{
    IEnumerator GetEnumerator();
}

foreach in 是語法糖:

IList<string> a = new List<string> { "大鳥", "小菜", "行李", "老外", "公車內部員工", "小偷" };

foreach (string item in a)
{
    Console.WriteLine($"{item} 請買車票!");
}

實際上編譯器會展開為:

IEnumerator<string> e = a.GetEnumerator();
while (e.MoveNext())
{
    Console.WriteLine($"{e.Current} 請買車票!");
}

本章小結#

迭代器模式分離了集合物件的遍歷行為,抽象出一個迭代器類來負責:

  • 不暴露集合的內部結構
  • 又可讓外部程式碼透明地訪問集合內部的資料

在訪問陣列、集合、列表,特別是資料庫資料操作時,迭代器模式是極其普遍的應用——但正因為太普遍,各種高階語言都做了封裝,反而給人「此模式本身不太常用」的錯覺。