for-in文でindexを使いたい場合、enumerated()
を使おうねと書かれている記事をよく見かけますが、indexとしては使えないケースもあります。コードを交えつつ解説していきたいと思います。
Swift3からC言語風の伝統的なforループが廃止されました。
こういうやつですね。
for var i = 0; i < 10; i++ {
print(i)
}
そこで、Swift3以降はindexを用いて配列にアクセスしたいときにenumerated()
を使うべし、というのがよく言われています。
let sushi = ["S", "U", "S", "H", "I", "🍣"]
for (i, value) in sushi.enumerated() {
print(sushi[i])
}
S
U
S
H
I
🍣
このコードは期待通り動くのですが、実はここに落とし穴があります。i
に入っているのは、一見配列のindexのように見えますが、実は単に0始まりのカウンターです。リファレンスにはこう書いてあります。
the integer part of each pair is a counter for the enumeration, not necessarily the index of the paired value
これは例えばArraySlice
を扱うときに問題になります。次のコードはIndex out of bounds
でエラーになります。
let sushi = ["S", "U", "S", "H", "I", "🍣"]
let sushiSub = sushi.dropFirst()
for (i, value) in sushiSub.enumerated() {
print(sushiSub[i])
}
なぜでしょうか。
dropFirst()
は先頭の要素を削除した部分配列を返すメソッドです。Sがなくなり、["U", "S", "H", "I", "🍣"]
のArraySlice
を返します。
ArraySlice
は元になった配列(ここでいうsushi
)のindexを保持するCollectionです。したがってsushiSub[1]
が"U"
となり、sushiSub[0]
は範囲外のindexとなります。前述したとおり**enumerated()
が返すものは単に0始まりのカウンター**なので、sushiSub[0]
にアクセスしてしまいIndex out of bounds
エラーとなる、というわけです。
さて、enumerated()
がindexを返さないのであれば、本当にindexを扱いたい場合はどうしたらよいのでしょうか。リファレンスには**zip(_, _)
使ってね**と書いてあります。
上のエラーになるコードのfor文は、下記のようにzipを使って書くと期待通り動きます。
for (i, value) in zip(sushiSub.indices, sushiSub) {
print(sushiSub[i])
}
U
S
H
I
🍣
動きました。indices
プロパティを使えばindex一覧をCountableRange
でとれるので、zipでまとめれば期待する挙動になります。
ちなみにsushiSub.indices
でなくsushiSub.startIndex..<sushiSub.endIndex
でもこの場合は大丈夫ですが、Collectionのindexは場合によっては飛び飛びの可能性もあり、startIndex
からendIndex
まで1ずつ増えていくことは保証されていないので、indices
を使うのがシンプルで汎用性のある書き方かなと思います。
enumerated()
を使うときには少し注意したほうが良いかも、というお話でした。公式リファレンスにはSet
のindexを扱う例も載っていますので興味のある方はそちらも合わせてどうぞ〜。
参考
enumerated() - Sequence | Apple Developer Documentation
SE-0007: Cスタイルのforループの廃止