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ループの廃止