オプションのバインディングによる、Swiftでの安全な(境界チェック済み)配列ルックアップ?


271

Swiftに配列があり、範囲外のインデックスにアクセスしようとすると、驚くほどのランタイムエラーが発生します。

var str = ["Apple", "Banana", "Coconut"]

str[0] // "Apple"
str[3] // EXC_BAD_INSTRUCTION

しかし、Swiftがもたらすすべてのオプションのチェーンと安全性を考えれば、次のようなことをするのは簡単です。

let theIndex = 3
if let nonexistent = str[theIndex] { // Bounds check + Lookup
    print(nonexistent)
    ...do other things with nonexistent...
}

の代わりに:

let theIndex = 3
if (theIndex < str.count) {         // Bounds check
    let nonexistent = str[theIndex] // Lookup
    print(nonexistent)   
    ...do other things with nonexistent... 
}

しかし、これはそうではありません-ol ' ifステートメントを使用して、インデックスが未満であることを確認および確認する必要がありstr.countます。

独自のsubscript()実装を追加しようとしましたが、呼び出しを元の実装に渡す方法、または添え字表記を使用せずに項目にアクセスする方法(インデックスベース)がわかりません。

extension Array {
    subscript(var index: Int) -> AnyObject? {
        if index >= self.count {
            NSLog("Womp!")
            return nil
        }
        return ... // What?
    }
}

2
これは少しOTであることに気づきますが、リストを含むあらゆる種類の境界チェックを実行するための明確な構文がSwiftにあったらいいと思います。このための適切なキーワードはすでにあります。たとえば、X in(1,2,7)...またはX in myArrayの場合
Maury Markowitz

回答:


652

Alexの回答には、質問に対する優れたアドバイスと解決策がありますが、この機能を実装するより良い方法に偶然遭遇しました。

Swift 3.2以降

extension Collection {

    /// Returns the element at the specified index if it is within bounds, otherwise nil.
    subscript (safe index: Index) -> Element? {
        return indices.contains(index) ? self[index] : nil
    }
}

Swift 3.0および3.1

extension Collection where Indices.Iterator.Element == Index {

    /// Returns the element at the specified index if it is within bounds, otherwise nil.
    subscript (safe index: Index) -> Generator.Element? {
        return indices.contains(index) ? self[index] : nil
    }
}

Swift 3のソリューションを思いついたHamish氏の功績です。

スウィフト2

extension CollectionType {

    /// Returns the element at the specified index if it is within bounds, otherwise nil.
    subscript (safe index: Index) -> Generator.Element? {
        return indices.contains(index) ? self[index] : nil
    }
}

let array = [1, 2, 3]

for index in -20...20 {
    if let item = array[safe: index] {
        print(item)
    }
}

45
これは間違いなく注目に値するものだと思います-素晴らしい仕事です。safe:違いを確認するために、含まれているパラメーター名が好きです。
Craig Otis

11
スイフト2(Xcodeの7)のように、これは少し微調整を必要とする:return self.indices ~= index ? self[index] : nil;
ティム

7
おそらくコーナーケースオンリープロンプト、それでもプロンプト:のため:上記の「安全な」添字バージョンは(スイフト2バージョンに対した)安全ではない場合があるスイフト3バージョンに関するCollectionタイプIndicesであるが隣接していません。たとえば、Setインデックス(SetIndex<Element>)でセット要素にアクセスする場合、>= startIndexand < endIndexであるインデックスの実行時例外が発生する可能性があります。その場合、安全な添え字は失敗します(たとえば、この不自然な例を参照)。
dfri

12
警告!この方法で配列をチェックすると、非常に負荷がかかります。contains従って、この方法は、このO(n)を作るすべてのインデックスを反復します。より良い方法は、インデックスとカウントを使用して境界をチェックすることです。
Stefan Vasiljevic

6
インデックスを生成し、それらを反復防ぐために(O(n))は、それがより良い使用の比較にあります(O(1)):return index >= startIndex && index < endIndex ? self[index] : nil Collection種類持っているがstartIndexendIndexありますComparable。もちろん、これは、たとえばインデックスが中央にない奇妙なコレクションでは機能しませんindices。ソリューションはより一般的なものです。
zubko

57

この動作が本当に必要な場合は、配列ではなく辞書が必要なように感じます。辞書を返すnil、それはそれらのキーが配列のキーは何も、することができるので、キーが辞書に存在するかどうかを知るためにはるかに困難ですので、理にかなって行方不明のキー、アクセスする際に必須:の範囲内0にしますcount。また、この範囲を反復処理することは非常に一般的であり、ループの各反復で実際の値が確実に得られます。

このように機能しないのは、Swift開発者が設計を選択したためだと思います。あなたの例を見てみましょう:

var fruits: [String] = ["Apple", "Banana", "Coconut"]
var str: String = "I ate a \( fruits[0] )"

すでにインデックスが存在することがわかっている場合は、配列を使用するほとんどの場合と同様に、このコードは最適です。ただし、添え字にアクセスすると戻る可能性があるnil場合は、Arraysubscriptメソッドの戻りの型をオプションに変更しました。これにより、コードが次のように変更されます。

var fruits: [String] = ["Apple", "Banana", "Coconut"]
var str: String = "I ate a \( fruits[0]! )"
//                                     ^ Added

つまり、範囲外のインデックスにアクセスすることはめったにないため、配列を繰り返し処理するたび、または既知のインデックスで何かを行うたびに、オプションをアンラップする必要があります。Swift設計者は、範囲外のインデックスにアクセスするときのランタイム例外を犠牲にして、オプションのアンラップを少なくすることを選択しました。そして、クラッシュはnil、データのどこかに予期していないことによって引き起こされる論理エラーよりも望ましいです。

そして私は彼らに同意します。したがって、デフォルトのArray実装を変更することはありません。これは、配列からの非オプション値を期待するすべてのコードを破壊するためです。

代わりに、サブクラス化しArray、オーバーライドsubscriptしてオプションを返すことができます。または、より実際的には、これArrayを行う非添え字メソッドで拡張できます。

extension Array {

    // Safely lookup an index that might be out of bounds,
    // returning nil if it does not exist
    func get(index: Int) -> T? {
        if 0 <= index && index < count {
            return self[index]
        } else {
            return nil
        }
    }
}

var fruits: [String] = ["Apple", "Banana", "Coconut"]
if let fruit = fruits.get(1) {
    print("I ate a \( fruit )")
    // I ate a Banana
}

if let fruit = fruits.get(3) {
    print("I ate a \( fruit )")
    // never runs, get returned nil
}

Swift 3アップデート

func get(index: Int) ->T? に置き換える必要があります func get(index: Int) ->Element?


2
の戻り値の型をsubscript()オプションに変更する際の問題について言及するための+1(および受け入れ)-これは、デフォルトの動作をオーバーライドする際に直面する主要な障害でした。(実際に動作させることはできませんでしたget()他のシナリオ(Obj-Cカテゴリ、だれか)では明らかな選択である拡張メソッドの作成を避けていましたが、get(ほど大きくなく[、この動作は、他の開発者がSwiftの添え字演算子に期待する動作とは異なる場合があることを明確にしてください。ありがとうございました!
Craig Otis 14

3
さらに短くするには、at()を使用します;)ありがとうございます。
hyouuu

7
Swift 2.0以降T、に名前が変更されましたElement。ちょうどフレンドリーなリマインダー:)
Stas Zhukovskiy

3
この議論を補足すると、境界チェックがSwiftに組み込まれてオプションを返さないもう1つの理由は、範囲nil外のインデックスから例外を発生させる代わりに返すことはあいまいであるためです。eg Array<String?>はコレクションの有効なメンバーとしてnilを返すこともできるため、これら2つのケースを区別することはできません。nil値を返すことができないことがわかっている独自のコレクションタイプがある場合、つまりアプリケーションに対してコンテキストがある場合、この投稿で回答されているように、安全な境界チェックのためにSwiftを拡張できます。
アーロン

美しく機能します
kamyFC

20

Nikita Kukushkinの答えを基にするには、配列のインデックスに安全に割り当てたり、それらから読み取ったりする必要がある場合があります。

myArray[safe: badIndex] = newValue

したがって、ここにNikitaの回答(Swift 3.2)の更新があります。これにより、safe:パラメータ名を追加することで、可変配列インデックスに安全に書き込むことができます。

extension Collection {
    /// Returns the element at the specified index iff it is within bounds, otherwise nil.
    subscript(safe index: Index) -> Element? {
        return indices.contains(index) ? self[ index] : nil
    }
}

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

        set(newValue) {
            if let newValue = newValue, indices.contains(index) {
                self[ index] = newValue
            }
        }
    }
}

2
非常に過小評価されている答え!これはこれを行う正しい方法です!
リード

14

Swift 2で有効

これはすでに何度も回答されていますが、Swiftプログラミングのファッションが進んでいるところで、より適切に回答を提示したいと思います。Crustyの言葉では、「protocol最初に考える」です。

•私たちは何をしたいですか?
- 与えられたインデックスの要素を、Arrayそれが安全な場合にのみ取得しnilます
それ以外の場合 •この機能は、何を基に実装する必要がありますか?
- Array subscriptINGの
どこからこの機能を取得しない•?
- モジュールでのその定義はそれを持っていstruct ArraySwift
ます•より一般的/抽象的なものはありませんか?
- それprotocol CollectionTypeは同様にそれを保証するものを採用します
•より一般的/抽象的なものは何ですか?
- それも採用protocol Indexableします...
•うん、私たちができる最高のように聞こえます。次に、この機能を拡張して、必要な機能を追加できますか?
- しかし、非常に限られたタイプ(no Int)とプロパティ(nocount)今すぐ作業する!
•それで十分でしょう。Swiftのstdlibは非常によくできています;)

extension Indexable {
    public subscript(safe safeIndex: Index) -> _Element? {
        return safeIndex.distanceTo(endIndex) > 0 ? self[safeIndex] : nil
    }
}

¹:真実ではないが、それはアイデアを与える


2
Swiftの初心者として、私はこの答えを理解していません。最後のコードは何を表していますか?それは解決策ですか、もしそうなら、実際にどのように使用しますか?
Thomas Tempelmann

3
申し訳ありませんが、この回答はSwift 3ではもはや有効ではありませんが、プロセスは確かに有効です。唯一の違いは、Collectionおそらく今は停止する必要があるということです:)
DeFrenZ

11
extension Array {
    subscript (safe index: Index) -> Element? {
        return 0 <= index && index < count ? self[index] : nil
    }
}
  • O(1)パフォーマンス
  • タイプセーフ
  • [MyType?]のオプションを正しく処理します(MyType ??を返します。これは、両方のレベルでラップ解除できます)
  • セットの問題を引き起こさない
  • 簡潔なコード

ここに私があなたのために実行したいくつかのテストがあります:

let itms: [Int?] = [0, nil]
let a = itms[safe: 0] // 0 : Int??
a ?? 5 // 0 : Int?
let b = itms[safe: 1] // nil : Int??
b ?? 5 // nil : Int?
let c = itms[safe: 2] // nil : Int??
c ?? 5 // 5 : Int?

10
  • 配列はnil値を格納する場合があるため、array [index]呼び出しが範囲外の場合にnilを返すことは意味がありません。
  • ユーザーが範囲外の問題をどのように処理したいかわからないため、カスタム演算子を使用しても意味がありません。
  • 対照的に、オブジェクトのアンラップには従来の制御フローを使用し、型の安全性を確保してください。

if index = array.checkIndexForSafety(index:Int)の場合

  let item = array[safeIndex: index] 

if index = array.checkIndexForSafety(index:Int)の場合

  array[safeIndex: safeIndex] = myObject
extension Array {

    @warn_unused_result public func checkIndexForSafety(index: Int) -> SafeIndex? {

        if indices.contains(index) {

            // wrap index number in object, so can ensure type safety
            return SafeIndex(indexNumber: index)

        } else {
            return nil
        }
    }

    subscript(index:SafeIndex) -> Element {

        get {
            return self[index.indexNumber]
        }

        set {
            self[index.indexNumber] = newValue
        }
    }

    // second version of same subscript, but with different method signature, allowing user to highlight using safe index
    subscript(safeIndex index:SafeIndex) -> Element {

        get {
            return self[index.indexNumber]
        }

        set {
            self[index.indexNumber] = newValue
        }
    }

}

public class SafeIndex {

    var indexNumber:Int

    init(indexNumber:Int){
        self.indexNumber = indexNumber
    }
}

1
興味深いアプローチ。何らかの理由SafeIndexがクラスであり、構造体ではないのですか?
-stef '20 /

8

スウィフト4

より伝統的な構文を好む人のための拡張:

extension Array {

    func item(at index: Int) -> Element? {
        return indices.contains(index) ? self[index] : nil
    }
}

インデックスにインデックスが含まれているかどうかを確認するために、配列要素をequatableに制約する必要はありません。
レオダバス

はい-良い点-deleteObjectなどの追加の安全なメソッドにのみ必要です
Matjan

5

安全な配列の取得、設定、挿入、削除が非常に便利であることがわかりました。他のすべてはすぐに管理が難しくなるため、エラーをログに記録して無視することを好みます。以下の完全なコード

/**
 Safe array get, set, insert and delete.
 All action that would cause an error are ignored.
 */
extension Array {

    /**
     Removes element at index.
     Action that would cause an error are ignored.
     */
    mutating func remove(safeAt index: Index) {
        guard index >= 0 && index < count else {
            print("Index out of bounds while deleting item at index \(index) in \(self). This action is ignored.")
            return
        }

        remove(at: index)
    }

    /**
     Inserts element at index.
     Action that would cause an error are ignored.
     */
    mutating func insert(_ element: Element, safeAt index: Index) {
        guard index >= 0 && index <= count else {
            print("Index out of bounds while inserting item at index \(index) in \(self). This action is ignored")
            return
        }

        insert(element, at: index)
    }

    /**
     Safe get set subscript.
     Action that would cause an error are ignored.
     */
    subscript (safe index: Index) -> Element? {
        get {
            return indices.contains(index) ? self[index] : nil
        }
        set {
            remove(safeAt: index)

            if let element = newValue {
                insert(element, safeAt: index)
            }
        }
    }
}

テスト

import XCTest

class SafeArrayTest: XCTestCase {
    func testRemove_Successful() {
        var array = [1, 2, 3]

        array.remove(safeAt: 1)

        XCTAssert(array == [1, 3])
    }

    func testRemove_Failure() {
        var array = [1, 2, 3]

        array.remove(safeAt: 3)

        XCTAssert(array == [1, 2, 3])
    }

    func testInsert_Successful() {
        var array = [1, 2, 3]

        array.insert(4, safeAt: 1)

        XCTAssert(array == [1, 4, 2, 3])
    }

    func testInsert_Successful_AtEnd() {
        var array = [1, 2, 3]

        array.insert(4, safeAt: 3)

        XCTAssert(array == [1, 2, 3, 4])
    }

    func testInsert_Failure() {
        var array = [1, 2, 3]

        array.insert(4, safeAt: 5)

        XCTAssert(array == [1, 2, 3])
    }

    func testGet_Successful() {
        var array = [1, 2, 3]

        let element = array[safe: 1]

        XCTAssert(element == 2)
    }

    func testGet_Failure() {
        var array = [1, 2, 3]

        let element = array[safe: 4]

        XCTAssert(element == nil)
    }

    func testSet_Successful() {
        var array = [1, 2, 3]

        array[safe: 1] = 4

        XCTAssert(array == [1, 4, 3])
    }

    func testSet_Successful_AtEnd() {
        var array = [1, 2, 3]

        array[safe: 3] = 4

        XCTAssert(array == [1, 2, 3, 4])
    }

    func testSet_Failure() {
        var array = [1, 2, 3]

        array[safe: 4] = 4

        XCTAssert(array == [1, 2, 3])
    }
}

3
extension Array {
  subscript (safe index: UInt) -> Element? {
    return Int(index) < count ? self[Int(index)] : nil
  }
}

上記の拡張機能を使用すると、インデックスが範囲外になるとnilが返されます。

let fruits = ["apple","banana"]
print("result-\(fruits[safe : 2])")

結果-nil


3

これは古い質問だと思います。この時点でSwift5.1を使用していますが、OPはSwift 1または2用でしたか?

今日はこのようなものが必要でしたが、1か所だけにフルスケールの拡張機能を追加したくなく、より機能的なもの(スレッドセーフかどうか)が必要でした。また、配列の終わりを過ぎている可能性のある負のインデックスから保護する必要もありませんでした。

let fruit = ["Apple", "Banana", "Coconut"]

let a = fruit.dropFirst(2).first // -> "Coconut"
let b = fruit.dropFirst(0).first // -> "Apple"
let c = fruit.dropFirst(10).first // -> nil

nilを使用したシーケンスについて議論している人のために、空のコレクションに対してnilを返すfirstおよびlastプロパティについてはどうしますか?

既存のものを手に取り、それを使用して希望する結果を得ることができるので、私はこれが好きでした。また、dropFirst(n)はコレクション全体のコピーではなく、単なるスライスであることも知っています。そして、すでに存在するfirstの動作が私を引き継ぎます。


1

これは良い考えではないと思います。範囲外のインデックスを適用しようとしないソリッドコードを作成することをお勧めします。

(上記のコードで示唆されているように)このようなエラーがサイレントに失敗nilすると、さらに複雑で扱いにくいエラーが発生する傾向があることを考慮してください。

使用したのと同じような方法でオーバーライドを行い、独自の方法で添え字を記述するだけです。唯一の欠点は、既存のコードに互換性がないことです。ジェネリックx [i](Cのようにテキストプリプロセッサもなし)をオーバーライドするフックを見つけるのは難しいでしょう。

私が考えることができる最も近いです

// compile error:
if theIndex < str.count && let existing = str[theIndex]

編集:これは実際に機能します。一発ギャグ!!

func ifInBounds(array: [AnyObject], idx: Int) -> AnyObject? {
    return idx < array.count ? array[idx] : nil
}

if let x: AnyObject = ifInBounds(swiftarray, 3) {
    println(x)
}
else {
    println("Out of bounds")
}

6
私は同意しません-オプションのバインディングのポイントは、条件が満たされた場合にのみ成功することです。(オプションの場合、値があることを意味します。)if letこの場合にを使用しても、プログラムは複雑にならず、エラーも扱いにくくなります。これは、従来の2つのステートメントによるif境界チェックと実際のルックアップを1行の圧縮されたステートメントに単純に圧縮したものです。インデックスが尋ねるような、範囲外であるために、それが正常である(特にUIでの作業)例があるNSTableViewためにselectedRow選択せずに。
Craig Otis

3
@Mundiこれは、OPの質問に対する回答ではなく、コメントのようです。
jlehr 2014

1
@CraigOtisよくわかりません。あなたは、することができます使用して「シングルライン凝縮声明」で簡潔例えば、この小切手を書くcountElementsか、OPがしていたとしてcount、だけではない方法で、配列の添え字を書く言語の定義。
ムンディ2014

1
@jlehrないかもしれません。提起された問題の意図や知恵を問うのは公正なゲームです。
ムンディ2014

2
@Mundi Heh、特に後で実際に質問に答えるために編集する場合。:-)
jlehr 2014

1

nil私の使用例では、配列にs を埋め込みました。

let components = [1, 2]
var nilComponents = components.map { $0 as Int? }
nilComponents += [nil, nil, nil]

switch (nilComponents[0], nilComponents[1], nilComponents[2]) {
case (_, _, .Some(5)):
    // process last component with 5
default:
    break
}

またsafe:、Erica Sadun / Mike Ashによるラベル付き添え字拡張子を確認してください:http : //ericasadun.com/2015/06/01/swift-safe-array-indexing-my-favorite-thing-of-the-new-week/


0

Swiftリストの「Commonly Rejected Changes」には、クラッシュではなくオプションを返すように配列の添え字アクセスを変更することに関する記述が含まれています。

作るArray<T>添字アクセスリターンをT?またはT!代わりに、T現在のアレイの動作は次のとおりです。意図的な、それは正確に範囲外の配列アクセスは、論理エラーであるという事実を反映して、。現在の動作を変更Arrayすると、アクセスが許容できないほど遅くなります。このトピックでは、来ている複数の前回が、受け入れられることは非常に考えにくいです。

https://github.com/apple/swift-evolution/blob/master/commonly_proposed.md#strings-characters-and-collection-types

したがって、基本的な添え字アクセスはオプションを返すように変更されません。

ただし、Swiftチーム/コミュニティは、関数または添え字のいずれかを使用し、新しいオプションの戻りアクセスパターンを配列に追加することを受け入れるようです。

これは、Swift Evolutionフォーラムで提案され、議論されています:

https://forums.swift.org/t/add-accessor-with-bounds-check-to-array/16871

特に、Chris Lattnerはアイデアに「+1」を与えました。

これについて最も頻繁に提案されているスペルは次のとおりです:yourArray[safe: idx]、これは私には素晴らしいようです。これを追加したことに対して私は非常に+1です。

https://forums.swift.org/t/add-accessor-with-bounds-check-to-array/16871/13

したがって、これはSwiftの将来のバージョンでそのまま使用できる可能性があります。このSwift Evolutionスレッドに貢献してほしいと思っている人なら誰でもお勧めします。


0

操作が失敗する理由を伝えるには、オプションよりもエラーの方が適しています。添え字はエラーをスローすることができないので、それはメソッドでなければなりません。

public extension Collection {
  /// - Returns: same as subscript, if index is in bounds
  /// - Throws: CollectionIndexingError
  func element(at index: Index) throws -> Element {
    guard indices.contains(index)
    else { throw CollectionIndexingError() }

    return self[index]
  }
}

/// Thrown when `element(at:)` is called with an invalid index.
public struct CollectionIndexingError: Error { }
XCTAssertThrowsError( try ["🐾", "🥝"].element(at: 2) )

let optionals = [1, 2, nil]
XCTAssertEqual(try optionals.element(at: 0), 1)

XCTAssertThrowsError( try optionals.element(at: optionals.endIndex) )
{ XCTAssert($0 is CollectionIndexingError) }

0

なぜ誰もが自動的に配列を拡張するためのセッターを備えた拡張機能を使用していない理由がわかりません

extension Array where Element: ExpressibleByNilLiteral {
    public subscript(safe index: Int) -> Element? {
        get {
            guard index >= 0, index < endIndex else {
                return nil
            }

            return self[index]
        }

        set(newValue) {
            if index >= endIndex {
                self.append(contentsOf: Array(repeating: nil, count: index - endIndex + 1))
            }

            self[index] = newValue ?? nil
        }
    }
}

使い方は簡単で、Swift 5.1以降で機能します

var arr:[String?] = ["A","B","C"]

print(arr) // Output: [Optional("A"), Optional("B"), Optional("C")]

arr[safe:10] = "Z"

print(arr) // [Optional("A"), Optional("B"), Optional("C"), nil, nil, nil, nil, nil, nil, nil, Optional("Z")]

注:配列を迅速に拡張する場合は、パフォーマンスコスト(時間/空間の両方)を理解する必要があります。ただし、小さな問題の場合は、SwiftにSwifting自体を停止させる必要がある場合があります。


-1

配列の簡単な拡張を行いました

extension Array where Iterator.Element : AnyObject {
    func iof (_ i : Int ) -> Iterator.Element? {
        if self.count > i {
            return self[i] as Iterator.Element
        }
        else {
            return nil
        }
    }

}

設計どおりに機能します

   if let firstElemntToLoad = roots.iof(0)?.children?.iof(0)?.cNode, 

-1

Swift 5の使用法

extension WKNavigationType {
    var name : String {
        get {
            let names = ["linkAct","formSubm","backForw","reload","formRelo"]
            return names.indices.contains(self.rawValue) ? names[self.rawValue] : "other"
        }
    }
}

に終わったが、本当に一般的にしたい

[<collection>][<index>] ?? <default>

しかし、コレクションはコンテキストに基づいているので、私はそれが適切だと思います。


この回答は承認された回答とどう違うのですか?私については、まったく同じように見えます(重複)。
Legonaftik

-1

配列から値を取得する必要があるだけで、パフォーマンスが少し低下することを気にしない場合(つまり、コレクションが大きくない場合)、関係しない辞書ベースの代替方法があります(あまりに一般的すぎて、テイスト)コレクション拡張:

// Assuming you have a collection named array:
let safeArray = Dictionary(uniqueKeysWithValues: zip(0..., array))
let value = safeArray[index] ?? defaultValue;
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.