Sponsor Link
環境&対象
- 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]
}
}
- AnimalList という形から、makeIterator で Iterator を作成するようにしました(Sequence という Protocol が、このことを要求する Protocol です)
- IteratorProtocol に準拠する MyIterator を宣言しています
- func next() -> Element? が IteratorProtocol の要求するメソッドです。struct なので、mutating を付与することが必要でした
- 次要素に該当する要素がない時は nil を返します
- defer を使うとスコープが終わる時の処理を記述できますので、animals[index] を返した後に、index += 1 が実行されます
defer の詳細は、以下の記事で。
[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 で使っているとなんとなく以下のような順序で評価されているイメージになりがちではないでしょうか
- 最初に、ループ対象リストを取得する
- リスト中の要素を順に 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]
}
}
- 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 につながっていきます。
AsyncSequence は、別記事で説明する予定です
まとめ:Iterator 再理解
- Iterator は Sequence に準拠した コレクションを走査するための要素
- for-in ループは、Iterator を使ったループに変換されている
- for-in ループの形で使われても、next は、実際に必要になったタイミングで呼ばれている
Swift 学習におすすめの本
詳解Swift
Swift の学習には、詳解 Swift という書籍が、おすすめです。
著者は、Swift の初期から書籍を出していますし、Swift の前に主力言語だった Objective-C という言語についても同様の書籍を出しています。
最新版を購入するのがおすすめです。
現時点では、上記の Swift 5 に対応した第5版が最新版です。
Swift ポケットリファレンス
Swift を学んでも、プログラミング言語の文法を全て記憶しておくことは無理なので、ちょっとした文法の確認をするために、リファレンス本を手元に置いておくと便利です。
Swift4 までしか対応していないので、相違点を理解して参照する必要があります。
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
Sponsor Link