軟體設計

Swift 迭代器模式:Sequence 的原理

使用 Swift 開發 App 時,我們每天會寫數十次像 for item in array 這樣的迴圈。因為太習慣了,直到嘗試用 for-in 迭代自訂型別並遇到編譯錯誤,才會好奇:「這個迴圈到底在內部做了什麼?」

閱讀 4 分鐘
Swift 迭代器模式:Sequence 的原理 封面圖

使用 Swift 開發 App 時,我們每天會寫數十次像 for item in array 這樣的迴圈。因為太習慣了,直到嘗試用 for-in 迭代自訂型別並遇到編譯錯誤,才會好奇:「這個迴圈到底在內部做了什麼?」

先說結論,Swift 的 for-in 迴圈是由 SequenceIteratorProtocol 這兩個協定共同運作的。Sequence 宣告「我可以被迭代」,IteratorProtocol 則負責實際取出值:「我會提供下一個值。」

本文會整理這兩個協定如何分工、for-in 如何在內部展開,以及如何自行建立自訂序列。若你覺得迭代器模式很難,請讀到最後。

Sequence 和 IteratorProtocol 有什麼不同?

先從最容易混淆的地方開始。兩者都與迭代有關,但職責明確不同。

IteratorProtocol 只有一項要求:mutating func next() -> Element?。每次呼叫會回傳下一個元素;沒有更多元素時則回傳 nil。

也就是說,迭代器是「攜帶狀態的游標」。它記住目前讀到的位置,每次呼叫 next() 就前進一格。

Sequence 位於更高一層,負責透過 makeIterator() 方法建立並交出迭代器。就像是在說:「想迭代我,就使用這個迭代器。」

總結如下。

  • IteratorProtocol:逐一取出值的游標。核心是 next()。
  • Sequence:建立游標的工廠。核心是 makeIterator()。

陣列、字典、Set、字串、Range……Swift 標準函式庫中可迭代的型別都遵循 Sequence。


for-in 在內部是如何展開的?

這是本文的核心。我們不經意使用的 for-in,其實是語法糖(syntactic sugar)。

以下是我們平常使用的迴圈。

for number in [1, 2, 3] {
    print(number)
}

編譯器大致會將這段程式碼轉換成以下形式來處理:用 makeIterator() 建立迭代器,並持續執行 while 迴圈,直到 next() 回傳 nil。

var iterator = [1, 2, 3].makeIterator()
while let number = iterator.next() {
    print(number)  // 1, 2, 3 依序輸出
}

for-in 不是魔法,歸根究柢只是 makeIterator() 與 next() 的組合。

原來最後就是這兩個呼叫
原來最後就是這兩個呼叫

理解這個結構後,就能自然明白為什麼只遵循 Sequence 的型別可以直接用於 for-in,因為 for-in 只需要 makeIterator()。


如何建立自訂序列?

親自實作一次就會更有概念。以下以能無限產生 Fibonacci 數列的序列為例。

最簡單的方法是在一個小型別中同時遵循 Sequence 和 IteratorProtocol。這樣 makeIterator() 的預設實作就會複製並回傳自身。

struct Fibonacci: Sequence, IteratorProtocol {
    var current = 0
    var nextValue = 1
    mutating func next() -> Int? {
        defer { (current, nextValue) = (nextValue, current + nextValue) }
        return current
    }
}

next() 的工作很單純:在回傳目前值之前,使用 defer 預先計算下一個狀態。

現在這個型別就能直接用於 for-in。因為是不定長度序列,所以必須自行限制迭代次數。

for n in Fibonacci().prefix(8) {
    print(n)  // 0 1 1 2 3 5 8 13
}
親自撰寫 Fibonacci 型別並放進 for-in
親自撰寫 Fibonacci 型別並放進 for-in

也別忘了可以直接使用 prefix、map、filter 等方法。遵循 Sequence 後,這些標準運算就會透過協定延伸自動提供,不必自行實作。


常見問題 Q&A

Q. 只遵循 Sequence 就可以嗎?IteratorProtocol 也一定需要嗎? A. 要執行 for-in,最終一定要有迭代器。不過如上例,若同一型別同時遵循兩者,就不必另外實作 makeIterator()。若想分離迭代狀態與集合,可將迭代器獨立成另一個 struct。

Q. 將序列用 for-in 迭代一次後,再迭代一次會怎樣? A. 陣列等值型別每次都會建立新的迭代器,因此能安全地迭代多次。但網路串流等消費後即消失的序列只能迭代一次,請特別注意。

Q. 它和 Collection 有什麼不同? A. Collection 是繼承 Sequence 的更高階概念,要求能以索引多次存取、計算 count 並保證順序等更多能力。若只需單向瀏覽值,Sequence 就足夠了。

畫出 Sequence、Iterator、Collection 的關係後,終於一目了然
畫出 Sequence、Iterator、Collection 的關係後,終於一目了然

原來一行 for-in 背後藏著 Sequence 與 IteratorProtocol 的清楚分工:迭代器負責游標,序列負責工廠。分清兩者後,建立自訂迭代型別就容易多了。

下次使用 for-in 時,不妨想一下「現在正在呼叫 next()」。也建議你從親自建立 Fibonacci 序列開始。

延伸閱讀