[Swift] Iterator をあらためて理解する

Swift

なんとなく 使わなくなりがち(?)な Iterator をあらためて理解してみます

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Monterey beta 12
  • Xcode 13 beta5
  • iOS 15

Iterator

コレクションの要素について一通り処理したいときに使うのが、Iterator です。

デザインパターンの名前でもあります。Wikipedia は、こちら

IteratorProtocol

Swift では Iterator は、IteratorProtocol に準拠している必要があります。

IteratorProtocol では、コレクションの要素として associatedType が決められ、next で次の要素が取得できる必要があります。すべての要素を取得した後では nil が返され、コレクションをすべて辿ったことがわかります。

このような Iterator を使って走査することができる コレクション は、Sequence プロトコルに準拠しています。

Iterator の取得方法

Sequence プロトコルに準拠していれば makeIterator を使うことで、そのコレクションに応じた Iterator を取得することができます。

# 走査対象となるコレクションが存在しなければ、Iterateor は意味がありませんので、単独で生成できるものではありません。

syntax sugar

Swift では、Iterator を直接使わなくとも、for-in という構文を使うことで、集合内の要素についての処理を記述することができます。


let animals = ["Antelope", "Butterfly", "Camel", "Dolphin"]
for animal in animals {
    print(animal)
}
// Prints "Antelope"
// Prints "Butterfly"
// Prints "Camel"
// Prints "Dolphin"

内部的には、Iterator を使う処理に展開されています。


var animalIterator = animals.makeIterator()
while let animal = animalIterator.next() {
    print(animal)
}
// Prints "Antelope"
// Prints "Butterfly"
// Prints "Camel"
// Prints "Dolphin"

自分で Iterator を作ってみる

通常の Iterator を使うだけでなく、IteratorProtocol に準拠させることで、自分で Iterator を作ることもできます。

Iterator を自作してみることで、実行タイミングを確認してみます。


import Foundation


struct AnimalList: Sequence {
    let animals = ["Eagle", "Dog", "Whale", "Cat", "Dolphin", "Elephant"]
    // (1)
    func makeIterator() -> MyIterator {
        return MyIterator(self)
    }
}

// (2)
struct MyIterator: IteratorProtocol {
    let animalList: AnimalList
    var index: Int = 0

    init(_ animalList: AnimalList) {
        self.animalList = animalList
    }

    // (3)
    mutating func next() -> String? {
        // (4)
        guard index < animalList.animals.count else { return nil }
        // (5)
        defer { index += 1 }
        return animalList.animals[index]
    }
}
コード解説
  1. AnimalList という形から、makeIterator で Iterator を作成するようにしました(Sequence という Protocol が、このことを要求する Protocol です)
  2. IteratorProtocol に準拠する MyIterator を宣言しています
  3. func next() -> Element? が IteratorProtocol の要求するメソッドです。struct なので、mutating を付与することが必要でした
  4. 次要素に該当する要素がない時は nil を返します
  5. defer を使うとスコープが終わる時の処理を記述できますので、animals[index] を返した後に、index += 1 が実行されます

defer の詳細は、以下の記事で。
Swift[Swift] defer 文の使い方

自分で作った Iterator を使ってみます。



let animals = AnimalList()

var ite = animals.makeIterator()

while let element = ite.next() {
    print(element)
}
// print-out
Eagle
Dog
Whale
Cat
Dolphin
Elephant

for animal in animals {
    print(animal)
}
// print-out
Eagle
Dog
Whale
Cat
Dolphin
Elephant

next の評価タイミング

ここで、next が評価されるタイミングを確認してみます。

for-in で使っているとなんとなく以下のような順序で評価されているイメージになりがちではないでしょうか

  1. 最初に、ループ対象リストを取得する
  2. リスト中の要素を順に closure に渡す

Iterator のコード中に以下のように print 文を入れてみます。


struct MyIterator: IteratorProtocol {
    let animalList: AnimalList
    var index: Int = 0

    init(_ animalList: AnimalList) {
        self.animalList = animalList
    }

    mutating func next() -> String? {
        guard index < animalList.animals.count else { return nil }
        // (1)
        print("Evaluating Next!")
        defer { index += 1 }
        return animalList.animals[index]
    }
}
コード解説
  1. next で次要素を返す直前に print 文をいれました

for-in ループを実行してみます。


et animals = AnimalList()

for animal in animals {
    print(animal)
}
// print-out
Evaluating Next!
Eagle
Evaluating Next!
Dog
Evaluating Next!
Whale
Evaluating Next!
Cat
Evaluating Next!
Dolphin
Evaluating Next!
Elephant

この結果から、Iterator の next は、呼ばれた時点で評価されることがわかります。

関数が呼ばれた時点で評価されることは ある意味 当たり前ですが、for-in ループになっていることで next が呼ばれるタイミングが分かりにくくなっています。

この next による 次要素の準備が時間のかかる処理の場合には、必要になったときに必要な取得処理を行う いわゆる Lazy loading 的な処理にできていることを意味しています。

これをもう少し進めて、next での準備時間がもっとかかるケースを想定すると next を非同期にしたくなります。これが AsyncSequence につながっていきます。

MEMO
AsyncSequence は、別記事で説明する予定です

まとめ:Iterator 再理解

Iterator 再理解
  • Iterator は Sequence に準拠した コレクションを走査するための要素
  • for-in ループは、Iterator を使ったループに変換されている
  • for-in ループの形で使われても、next は、実際に必要になったタイミングで呼ばれている

Swift 学習におすすめの本

詳解Swift

Swift の学習には、詳解 Swift という書籍が、おすすめです。

著者は、Swift の初期から書籍を出していますし、Swift の前に主力言語だった Objective-C という言語についても同様の書籍を出しています。

最新版を購入するのがおすすめです。

現時点では、上記の Swift 5 に対応した第5版が最新版です。

Swift ポケットリファレンス

Swift を学んでも、プログラミング言語の文法を全て記憶しておくことは無理なので、ちょっとした文法の確認をするために、リファレンス本を手元に置いておくと便利です。

注意
Swift4 までしか対応していないので、相違点を理解して参照する必要があります。

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です