Swiftでアプリを作っていると、for item in arrayのようなループを1日に何十回も書きます。あまりに自然に使うため、カスタム型をfor-inで回そうとしてコンパイルエラーに遭遇して初めて疑問に思うこともあるでしょう。「このループの中では、一体何が起きているのでしょうか?」
結論から言うと、Swiftのfor-inループはSequenceとIteratorProtocolという2つのプロトコルが連携して動いています。Sequenceは「自分は反復可能です」と宣言し、IteratorProtocolは「次の値を1つ返します」と実際に値を取り出します。
この記事では、2つのプロトコルの役割分担、for-inが内部でどのように展開されるか、そしてカスタムシーケンスの作り方まで整理します。イテレータパターンを難しく感じていた方は、ぜひ最後まで読んでみてください。
SequenceとIteratorProtocolの違いとは?
まず、最も混乱しやすい点から見ていきましょう。どちらも反復に関係しますが、役割は明確に異なります。
IteratorProtocolの要件は1つだけです。mutating func next() -> Element?です。呼び出すたびに次の要素を1つ返し、返すものがなくなるとnilを返します。
つまり、イテレータは状態を保持するカーソルです。どこまで読み取ったかを記憶し、next()が呼ばれるたびに1つ先へ進みます。
Sequenceは、もう少し上位の概念です。makeIterator()メソッドでイテレータを作成して渡す役割を担います。「自分を反復したいなら、このイテレータを使ってください」と伝えるわけです。
まとめると、次のとおりです。
- IteratorProtocol:値を1つずつ取り出すカーソル。中心となるのはnext()。
- Sequence:そのカーソルを作るファクトリ。中心となるのはmakeIterator()。
配列、辞書、Set、文字列、Rangeなど、Swift標準ライブラリの反復可能な型はすべてSequenceに準拠しています。
for-inは内部でどう展開されるのか?
ここがこの記事の核心です。何気なく使っているfor-inは、実はシンタックスシュガーです。
次のコードは、普段使っているループです。
for number in [1, 2, 3] {
print(number)
}
コンパイラは、このコードをおおよそ次の形に変換して処理します。makeIterator()でイテレータを作り、next()がnilを返すまでwhileループを回すのです。
var iterator = [1, 2, 3].makeIterator()
while let number = iterator.next() {
print(number) // 1, 2, 3 の順番で出力
}
for-inは魔法ではありません。結局はmakeIterator()とnext()という2つの組み合わせにすぎません。
この構造を理解すると、Sequenceだけに準拠した型をfor-inにそのまま渡せる理由も自然に分かります。for-inに必要なのはmakeIterator()だけだからです。
カスタムシーケンスはどう作るのか?
実際に作ってみると、仕組みをより確実に理解できます。ここでは、フィボナッチ数列を無限に生成するシーケンスを例にします。
小さな型1つで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
}
prefixやmap、filterなどのメソッドを無料で使える点も重要です。Sequenceに準拠すると、こうした標準操作がプロトコル拡張によって付いてきます。自分で実装する必要はありません。
よくある疑問Q&A
Q. Sequenceだけに準拠すればよいですか?IteratorProtocolも必須ですか? A. for-inを回すには、最終的にイテレータが必要です。ただし、上の例のように1つの型で両方に準拠すれば、makeIterator()を別途実装する必要はありません。反復状態とコレクションを分離したい場合は、イテレータを別のstructに切り出します。
Q. 一度for-inで回したシーケンスを、もう一度回すとどうなりますか? A. 配列のような値型は毎回新しいイテレータを作るため、複数回安全に反復できます。一方、ネットワークストリームのように消費すると消えるシーケンスは1回しか回せないので注意してください。
Q. Collectionとは何が違いますか? A. CollectionはSequenceを継承した上位概念です。インデックスによる複数回のアクセス、countによる要素数の取得、順序の保証など、より多くの要件があります。値を一方向に走査するだけならSequenceで十分です。
for-inの1行の裏には、SequenceとIteratorProtocolによる明確な役割分担があります。イテレータはカーソル、シーケンスはファクトリです。この2つを区別できると、カスタム反復型を作るのがずっと簡単になります。
次にfor-inを使うときは、「今、next()が呼ばれているんだな」と一度思い浮かべてみてください。まずはフィボナッチシーケンスを自分で作ってみることをおすすめします。

