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

Cra*_*tis 252 xcode swift

如果我在Swift中有一个数组,并尝试访问超出范围的索引,则会出现一个不足为奇的运行时错误:

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

str[0] // "Apple"
str[3] // EXC_BAD_INSTRUCTION
Run Code Online (Sandbox Code Playgroud)

但是,我会想到Swift带来的所有可选链接和安全性,这样做会很简单:

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

代替:

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

但事实并非如此 - 我必须使用ol' if语句来检查并确保索引小于str.count.

我尝试添加自己的subscript()实现,但我不知道如何将调用传递给原始实现,或者不使用下标符号来访问项目(基于索引):

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

Nik*_*kin 600

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
    }
}
Run Code Online (Sandbox Code Playgroud)

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
    }
}
Run Code Online (Sandbox Code Playgroud)

感谢Hamish 提出Swift 3的解决方案.

斯威夫特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
    }
}
Run Code Online (Sandbox Code Playgroud)

let array = [1, 2, 3]

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

  • 我认为这绝对值得关注 - 干得好.我喜欢包含的`safe:`参数名称以确保区别. (38认同)
  • 从Swift 2(Xcode 7)开始,这需要一点调整:`return self.indices~ = index?self [index]:nil;` (11认同)
  • 为了防止生成索引并对其进行迭代 (O(n)),最好使用比较 (O(1)):`return index &gt;= startIndex &amp;&amp; index &lt; endIndex ? self[index] : nil `Collection` 类型有 `startIndex`, `endIndex` 它们是 `Comparable`。当然,这不适用于一些奇怪的集合,例如,中间没有索引,使用 `indices` 的解决方案更通用。 (9认同)
  • 关于夫特3版本:可能的角的病例仅-提示,但提示仍然:存在这样的情况,其中"安全"的下标以上版本不是安全的(而夫特2版本是):用于`Collection`类型"指数"不是连续的.例如,对于`Set`实例,如果我们要通过索引(`SetIndex <Element>`)访问set元素,我们可以运行`= startIndex`和`<endIndex`的索引的运行时异常,在这种情况下安全下标失败(参见例如[这个人为的例子](https://gist.github.com/dfrib/85398f9d3d5bfc9757905b499d79e26f)). (6认同)
  • 嘿,我已经更新了Swift 3的答案.我会坚持使用Swift 2一段时间,所以如果有任何问题,请随意指出. (4认同)
  • 原因是主观的.我把我的实现读作"数组的索引包括索引".它简洁,对我来说,似乎比通过比较进行的边界检查更简单.我也喜欢玩新的API.您可以自由地用您的实现替换我的实现,但是您可能还想添加对负索引的检查以完全"安全". (3认同)
  • 警告!用这种方法检查数组可能非常昂贵。“包含”方法将遍历所有索引,从而使其成为O(n)。更好的方法是使用索引和计数来检查界限。 (3认同)
  • @JonColverson是的,即使对于极端情况也应该是安全的,但是注意到我们放松了`O(1)`随机索引访问(我们做了,但是,对于上面的'contains`Swift 2版本),这应该是'但是,除非在具有大型阵列的某些HPC应用程序中工作,否则这是一个问题.对于另一个替代方案(类似于"O(n)"),它对应于上面Swift 2版本的更直接的Swift 2-> 3翻译,请参阅[此问答中的答案](http://stackoverflow.com/ a/40331858/4573247)(使用`contains`也允许"短路",就像你明确的`while`循环解决方案一样). (2认同)
  • 有人可以解释 Swift 3 实现的类型约束,以及它们是如何确定的吗?没有它们,Swift 会尝试使用错误的 `contains()` 实现;它尝试使用 `contains(where: _)`。我正在挖掘参考文档以跟随(继承的)关联类型,但我不确定为什么提供的约束解决了这个问题。你是怎么推导出来的? (2认同)

Ale*_*yne 56

如果你真的想要这种行为,它就像你想要一个Dictionary而不是一个数组.字典nil在访问丢失的密钥时返回,这是有道理的,因为要知道密钥是否存在于字典中要困难得多,因为这些密钥可以是任何东西,在数组中密钥必须在以下范围内:0to count.迭代这个范围是非常常见的,你可以绝对肯定在循环的每次迭代中都有一个真正的值.

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

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

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

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

这意味着每次迭代数组时都需要解包一个可选项,或者使用已知索引执行任何其他操作,因为很少有人可以访问超出范围的索引.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
}
Run Code Online (Sandbox Code Playgroud)

Swift 3更新

func get(index: Int) ->T? 需要被替换 func get(index: Int) ->Element?

  • 截至Swift 2.0`T`已更名为`Element`.只是一个善意的提醒 :) (7认同)
  • 为了使它更短,我用at();)谢谢! (3认同)
  • 在此讨论中,为什么不将边界检查引入Swift来返回可选值的另一个原因是因为返回`nil'而不是导致越界索引中的异常是模棱两可的。由于例如Array &lt;String?&gt;也可能返回nil作为集合的有效成员,因此您将无法区分这两种情况。如果您拥有自己的集合类型,并且知道它永远不会返回`nil'值(也就是应用程序的上下文),那么您可以扩展Swift进行安全边界检查,如本文所述。 (3认同)
  • +1(和接受)提及将“ subscript()”的返回类型更改为可选类型的问题-这是覆盖默认行为所面临的主要障碍。(我实际上根本无法使它正常工作。*)我避免编写`get()`扩展方法,这在其他场景(Obj-C类别,有人吗?)中是显而易见的选择,而在`get( `不会比`[`大很多,并且很清楚地表明行为可能与其他开发人员可能期望的Swift下标运算符有所不同。谢谢! (2认同)

DeF*_*enZ 14

在Swift 2中有效

虽然已经有很多次回答了这个问题,但我想在Swift编程的时尚方面提出更多的答案,用Crusty的话来说就是:"先想想protocol"

•我们想做什么?
- 只有在安全的情况下才能获得给定索引的元素Array,nil否则
•此功能应该什么为基础?
- Array subscriptING
•哪里不从得到这个功能吗?
- struct ArraySwift模块中的定义有它
•没有更通用/抽象的东西?
- 它采用protocol CollectionType哪种方式确保它
•没有更通用/抽象的东西?
- 它也采用protocol Indexable......
•是的,听起来像我们能做的最好.我们可以扩展它以获得我们想要的功能吗?
- 但我们现在有非常有限的类型(没有Int)和属性(没有count)!
•这就够了.Swift的stdlib做得很好;)

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

¹:不是真的,但它提出了这个想法

  • 对不起,这个答案对Swift 3来说已经无效了,但过程肯定是.唯一的区别是现在你应该停在`Collection`可能:) (3认同)
  • 作为Swift新手,我不明白这个答案。最后的代码代表什么?那是一个解决方案,如果是的话,我该如何实际使用它? (2认同)

Saf*_*ive 13

基于Nikita Kukushkin的答案,有时您需要安全地分配数组索引以及从它们读取,即

myArray[safe: badIndex] = newValue
Run Code Online (Sandbox Code Playgroud)

所以这里是对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
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 答案被大大低估了!这是正确的方法! (2认同)
  • 我很久以前就复制了这个答案,因为我的项目中删除了“MutableCollection”,所以我回来了。我恐怕不能给你更多的赞了! (2认同)

小智 10

  • 因为数组可能存储nil值,所以如果数组[index]调用超出范围则返回nil是没有意义的.
  • 因为我们不知道用户如何处理越界问题,所以使用自定义运算符是没有意义的.
  • 相比之下,使用传统的控制流程来展开物体并确保类型安全.

if let index = array.checkIndexForSafety(index:Int)

  let item = array[safeIndex: index] 
Run Code Online (Sandbox Code Playgroud)

if let index = array.checkIndexForSafety(index:Int)

  array[safeIndex: safeIndex] = myObject
Run Code Online (Sandbox Code Playgroud)
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
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 有趣的方法。为什么 `SafeIndex` 是一个类而不是一个结构? (2认同)

Bra*_*key 10

斯威夫特 5.x

扩展意味着RandomAccessCollection这也可以ArraySlice通过单个实现来实现。我们使用startIndexendIndex作为数组切片,使用来自底层父级的索引Array

public extension RandomAccessCollection {

    /// Returns the element at the specified index if it is within bounds, otherwise nil.
    /// - complexity: O(1)
    subscript (safe index: Index) -> Element? {
        guard index >= startIndex, index < endIndex else {
            return nil
        }
        return self[index]
    }
    
}
Run Code Online (Sandbox Code Playgroud)


the*_*utz 7

extension Array {
    subscript (safe index: Index) -> Element? {
        return 0 <= index && index < count ? self[index] : nil
    }
}
Run Code Online (Sandbox Code Playgroud)
  • O(1)表现
  • 类型安全
  • 正确处理[MyType?]的Optionals(返回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?
Run Code Online (Sandbox Code Playgroud)


Mat*_*jan 7

斯威夫特4

对于那些喜欢更传统语法的人的扩展:

extension Array {

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


Tra*_*ggs 7

我意识到这是一个老问题。我现在使用的是 Swift5.1,OP 是针对 Swift 1 还是 2?

我今天需要这样的东西,但我不想只为一个地方添加一个完整的扩展,想要一些更实用的东西(更线程安全?)。我也不需要保护负索引,只需要保护那些可能超过数组末尾的索引:

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
Run Code Online (Sandbox Code Playgroud)

对于那些争论带有 nil 的 Sequences 的人,对于空集合返回 nil的firstlast属性,您会怎么做?

我喜欢这个,因为我可以抓住现有的东西并使用它来获得我想要的结果。我也知道 dropFirst(n) 不是整个集合副本,只是一个切片。然后 first 已经存在的行为接管了我。


Iva*_*Rep 5

我发现安全的数组get,set,insert,remove非常有用。我更喜欢记录日志并忽略错误,因为其他所有问题很快都会变得难以管理。完整代码如下

/**
 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)
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

测验

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])
    }
}
Run Code Online (Sandbox Code Playgroud)