[Swift] Arrayのインデックスに範囲外の値を入れてもクラッシュしないようにする

Written by じび on 10月 22nd, 2017

概要

配列の要素のアクセスにて、クラッシュする”[]”の代わりに、nilを返す”[safe: ]”を追加して使う方法です。

序文

SwiftではArrayのインデックスに範囲外の値を指定するとクラッシュします。

    let array = ["A", "B", "C"]
    let index = 3
    let item = array[index]  // EXC_BAD_INSTRUCTION でクラッシュ

そのため、要素にアクセスする前に、インデックスのチェックを行う方法があります。

    let array = ["A", "B", "C"]
    let index = 3
    guard array.indices.contains(index) else {
        // インデックスの範囲外なら、nilを返す
        return nil
    }
    let item = array[index]  // 実行されない
}

面倒なのでついついサボって省略してしまい、後でクラッシュすることがあります。

実装

そこで、安全にアクセスできる”[safe: ]”を追加して、”[]”の代わりに使います。
これはインデックスが範囲外の場合、nilを返してくれます。
“[]”(subscript(Self.Index))はCollectionプロトコルのメソッドなので、同様にCollectionプロトコルに追加します。

extension Collection {
    subscript (safe index: Index) -> Element? {
        return indices.contains(index) ? self[index] : nil
    }
}

用例

“[]”の代わりに”[safe: ]”を使うと、インデックスが範囲外でもクラッシュせずにnilを返します。

    let array = ["A", "B", "C"]
    let index = 3
    let item = array[safe: index]  // nilを返す

注意

ちなみに配列の要素にnilが入っていた場合、戻り値の型は”Type??”になります。

    let array = ["A", "B", "C", nil]
    let index = 3
    let item = array[safe: index]  // item : String??

itemに対する最初のunwrapでnilならインデックスが範囲外、2度目のunwrapでnilなら要素がnilです。

    guard let firstUnwrappedItem = item else {
        return nil  // インデックスが範囲外
    }
    
    guard let secondUnwrappedItem = firstUnwrappedItem else {
        return nil  // 要素がnil
    }

追記

「そもそも範囲外のインデックスにアクセスするのはバグなのだから、nilを返すよりクラッシュさせてしまうべき」との意見もあるのですが、ユーザーにとってはアプリがクラッシュするのは最悪の事態なので、クラッシュするよりも動かない(nilチェックでリターンさせているとほとんどこの動きになります)方がはるかに良いと考えてます。

これは組込み系でよくある、フェイルセーフの考え方が主軸になっています。

参考

xcode – Safe (bounds-checked) array lookup in Swift, through optional bindings? – Stack Overflow

 

Leave a Comment