提问者:小点点

Swift中通过可选绑定的安全(边界检查)数组查找?


如果我在 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语句来检查并确保索引小于< code>str.count。

我尝试添加自己的< code>subscript()实现,但是我不确定如何将调用传递给原始实现,或者在不使用下标符号的情况下访问项目(基于索引):

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

共3个答案

匿名用户

Alex的回答对这个问题有很好的建议和解决方案,但是,我碰巧发现了实现此功能的更好方法:

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
    }
}
let array = [1, 2, 3]

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

匿名用户

如果你真的想要这种行为,它闻起来就像你想要一个字典而不是数组。字典在访问丢失的键时返回nil,这是有道理的,因为很难知道字典中是否存在键,因为这些键可以是任何东西,在数组中,键必须在以下范围内:0to。在这个范围内迭代是非常常见的,在这个范围内,您可以绝对确定在循环的每次迭代中都有一个实值。

我认为它不能以这种方式工作的原因是Swift开发人员做出的设计选择。举个例子:

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

如果您已经知道索引存在,就像您在使用数组的大多数情况下所做的那样,这段代码很棒。但是,如果访问下标可能会返回nil,那么您已将Array下标方法的返回类型更改为可选。这将您的代码更改为:

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

这意味着每次遍历数组或使用已知索引执行任何其他操作时,您都需要解包装可选项,只是因为您很少访问越界索引。Swift设计者选择了较少的可选项解包装,以访问越界索引时的运行时异常为代价。崩溃比您在某处的数据中没有预料到的nil引起的逻辑错误更可取。

我同意他们的看法。因此,您不会更改默认的 Array 实现,因为您会破坏所有需要数组中非可选值的代码。

相反,您可以子类<code>Array</code>,并重写<code>下标</code〕以返回可选。或者,更实际地,您可以使用非下标方法扩展<code>Array</code>。

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
}

func-get(索引:Int)-

匿名用户

根据Nikita Kukushkin的回答,有时您需要安全地将数组索引赋值并从中读取,即

myArray[safe: badIndex] = newValue

因此,这是对Nikita答案(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
    }
}

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
            }
        }
    }
}