Swift の indices と endIndex と enumerate に思いを馳せる
Swift プログラミング
Swift でインデックスを扱うときに思い浮かぶ indices, startIndex/endIndex, enumerate 周りの特徴や使い分け方とかにいろいろ思いを馳せてみました。
先日に拝見した @JohnEstropiaさん
の Swiftならこう書くシリーズ 10選
の影響で CollectionType
の indices
が心に残るこの頃でした。indices
というのは、CollectionType
で規定されている、配列の要素を取得するのに使うインデックスの全てを網羅した Range
です。
そんな折、手持ちのプロジェクトを Swift 2.2 対応していて indices
に書き換えるべき場面に出会って嬉しくなったり、そこから再び indices
と startIndex
と endIndex
、そして enumerate
について思いを巡らせてみたりしたので、今回はそれについて記してみることにしました。
まずは Objective-C
話を進めるにあたって、まずは Objective-C での一般的な、配列を順次取得して取得する方法は次のようでした。
NSArray* items = @[1, 2, 3, 4];
for (NSUInteger index = 0; index <= items.count - 1; ++index)
{
NSInteger item = [items objectAtIndex: index];
}
このコードの要点として、配列 NSArray
のインデックス番号が 0 から始まることが大事な担保になっているように感じます。その上でインデックスを意味する数値を作って使用している印象です。
Objective-C では NSFastEnumeration
の登場によって for ... in
構文を使った簡素な書き方ができるようになってますけど、それに対応していない型では上記の方法になるので、今回はインデックスに注目したお話として上記の例を挙げてみました。他にも真ん中の判定式を index != items.count
としたり index < items.count
としたりできます。
C 言語スタイル for 構文
上記の Objective-C の流れを汲んで書くと、Swift 2.2 以前までは、次のような感じになります。
let items: Array<Int> = [1, 2, 3, 4]
for var index = 0; index <= items.count - 1; ++index
{
let item: Int = items[index]
}
こちらの場合も、配列 Array
のインデックス番号が 0 から始まることが大事な担保になっているように感じます。
それを加味するかどうかはまず置いておいて、ここで使われる ++index
や、そもそもの for ;;
構文が Swift 2.2 からは非推奨とされたことによって、インデックスの扱い方について改めて意識を向けるきっかけが生まれたことは嬉しいことのように感じます。
Swift におけるインデックスを表現する形
そうして Swift における配列のインデックスを表現する方法に目を向けた時、自分には startIndex
と endIndex
による方法、それと indices
プロパティによる方法、そして enumerate
メソッドによる方法、その3つの方法が見つかりました。
startIndex と endIndex
まずは startIndex
と endIndex
による方法に目を向けてみます。C 言語スタイルの for
構文が廃止されることを考えると、今には合わない方法ですけど、インデックスを捉える上で大事なところがありそうなので挙げておきます。
ちなみに startIndex
と endIndex
というのは、配列が準拠する Indexable
で規定されている、添字の開始と終端を意味するプロパティです。ちなみに successor
というのは、あるインデックスの次のインデックスを取得するメソッドです。
for var index = items.startIndex; index != items.endIndex; index = index.successor() {
let item: Int = items[index]
}
ここでまず大事になるのが startIndex
です。最初に紹介した方法は 配列の先頭インデックスが 0 になる
ことを前提にコーディングしていましたけど、この startIndex
は Indexable
によって 最初のインデックス
であることが約束されています。
そして endIndex
になるまで繰り返すというところも大事なところ。Indexable
は startIndex
から endIndex
までインデックスを辿っていけることが約束されることにもなるので、インデックスの大小関係を考慮しなくても endIndex
に到達したかで終了条件を確実に判断できます。しかも endIndex
は終端、つまり有効な値が取れた次のところを指すものなので、終端と一致したタイミングで処理を終了できます。
もうひとつ重要なのが、インデックスを進めるのに使う successor
です。配列のインデックスの場合、現実的には 0 から始まる通し番号
なので番号を 1 ずつ足していけば終端まで辿りつくのですけど、それは事実上の話であって、先ほどの 配列のインデックスは 0 から始まる
というのと同じ感覚、極論すれば たまたま次のインデックスが取れている
とも言えます。これが successor
であれば、次のインデックスが取得できることが ForwardIndexType
によって約束されるので、確実に次のインデックスへ駒を進めることが可能になります。
このように startIndex
と endIndex
と、加えて successor
によって個々の配列事情に依存しないインデックスを辿るコードを記載できます。
indices
そんな風にしてインデックスについて眺めてみた時、Swift でインデックスを扱う別の方法に indices
というのがあります。これは CollectionType
に規定されているもので、これを使うとその配列が扱うインデックス値を範囲として Range<Index>
で取得できます。
let range: Range<Int> = items.indices
このとき、先ほど説明した startIndex
と、こうして indices
で取得したインデックスと endIndex
とでは観点が大きく異なるようです。前者はインデックスを順番に取り出すことが目的なのに対し、後者は取り得るインデックス値の範囲を表現するのが目的のように見受けられます。もう少し言うと、前者は 制御目的としている
のに対して、後者は 値そのものに着目している
ともとれそうです。
Swift の範囲は first
と last
を使って、最初の値と最後の値を取得できるようになっていますが、これを使うとそんな観点の違いが良く見えてくるように思います。
items.startIndex == items.indices.first! // true
items.endIndex == items.indices.last! // false
このように、startIndex
と indices.first!
については同じ値を示すのですけど、endIndex
と indices.last!
については値が異なってきます。これは前者が 制御の終端を示すもの
なのに対して、後者が いちばん最後の値
であると捉えると、合点の行く出来事のように思います。
つまりこうして考えた時に、例えば インデックスを順次辿って処理する
みたいな観点のときには startIndex
と endIndex
とが向いていて、インデックス値を取りたいという観点のときには indices
が向いている、とも言えそうです。もう少し言うと 向いてる
というより 適切
と表現したほうが良いかもしれません。
もちろん、そうして取り出したインデックス値を辿って処理をするときには Range
でも CollectionType
の startIndex
と endIndex
を使うことにはなるのですけど、それらは for ... in
構文を使うと存在感を隠せるので、そうしてみたとき、indices
を使うコードがインデックス値を扱っているということを明確に主張できていて、とても適切なコードが書けているように感じられます。
for index in items.indices {
let item: Int = items[index]
}
先ほど C 言語スタイルの for
構文を使ったコードが、このようにまさにインデックスの値に着目しているコードで表現できるようになりました。
何も考えなくても インデックスを順番にたどっている
ことが見るだけでわかる、それが何より嬉しいところのように思います。そして大事なところとして、この indices
を使った方法も、開始インデックスが 0 であることを前提にする場合と違って、確実に実際のインデックス値で表現されていることが CollectionType
によって約束されている点も大事なところです。
余談
ちなみに Swift の配列はインデックスを使わなくても順次値を取り出すことが可能です。その場合は次のように、配列をそのまま for ... in
構文に指定することになります。
for item in items {
}
今回はインデックス値に注目したお話なのでこれは余談になりますけれど、普段にコードを書く中で インデックス値を、それが該当する要素を取り出す だけにしか使わない場合はこのように書くことで、インデックスに関して不必要に考える手間を省くことができ、更に明確なコードになるのが嬉しいところです。
enumerate
そしてもうひとつ、インデックスを処理に使おうと考えた時に候補に上がる可能性があるのが enumerate
メソッドです。こちらは SequenceType
で規定されているメソッドですけど、配列が準拠している CollectionType
も SequenceType
の性質を全て持っているので利用できます。
このメソッドの大きな特徴は、配列の要素を順次取り出せる EnumerateSequence
型が得られるのですけど、この時この型のインスタンスからは、単に配列の要素だけではなく、その要素が何番目に登場したかも合わせて取り出すことができます。つまり、取り出せる値は (インデックス番号, 該当する要素)
というタプル型になります。
for (index, item) in items.enumerate() {
}
この時、とても大事になるのが、先ほど紹介した startIndex
や indices
のときとは違って、取得できるインデックス番号が 0 から始まる通し番号
になるというところです。つまりその番号が実際のインデックスと一致することは約束しないというところに注意する必要があります。
この場合に得られるインデックス番号は、元になった配列とは全然関係性のないインデックス番号、単なる通し番号ということになります。
enumerate の使いどころ
そんな特色をおさえると、配列のインデックスを操作するために使うのには向いていないことが見えてきますが、それと同時に間違いなく登場順番に着目できるという利点も見えてきます。
例えば、複数のセルを表現する cells
変数があったとして、それの 偶数番目と奇数番目でセルの色を変えたい
みたいなときに、もしそれを indices
を使って実際のインデックス番号で判断すると、もしも cells
配列が 1 から始まるインデックスだったりしたときに、色付けする場所がおかしくなります。
そんな場合でも enumerate
を使えば、最初のインデックスが必ず 0 から始まるので、どんなインデックス番号で始まる配列であっても登場順番に合わせた確実な色付けを保証できます。
for (rowIndex, cellItem) in cellItems.enumerate() {
func isRowEven(index: Int) -> Bool {
return index & 1 == 0
}
if isRowEven(rowIndex) {
cellItem.backgroundColor = NSColor.brownColor()
}
else {
cellItem.backgroundColor = NSColor.whiteColor()
}
}
ちなみに普通のインデックス番号を利用して、何番目に登場するものかを判定することも可能です。ただし、その場合に 0 を前提としても大抵の配列では正しく動くと思いますけど厳密ではなくて、通し番号として正しく扱うためには次のように distanceTo
を使うことになります。
for index in cellItems.indices {
let cellItem = cellItems[index]
let rowIndex = cellItems.startIndex.distanceTo(index)
func isRowEven(index: Int) -> Bool {
return index & 1 == 0
}
if isRowEven(rowIndex) {
cellItem.backgroundColor = NSColor.brownColor()
}
else {
cellItem.backgroundColor = NSColor.whiteColor()
}
}
厳密にはこのようになって、先ほどの enumerate
を使う場合と比べて随分コードが複雑になった印象がします。
まとめ
こんな感じで 配列のインデックスを処理するならこれ一択! みたいに考えずに、それぞれの表現方法の性格を捉えて適したところで使うことで、随分とスマートで誤解(バグ)を生みにくいコードになることが見て取れたような気がします。
とても些細なところですけど、こういうところに目を向けてみると、コードの安定性もそうですし、それぞれを使い分ける価値判断ができるようになって、コードを書くのがいっそう楽しくなりそうです。