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

时间:2014-08-15 15:20:54

标签: xcode 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&#39; if语句用于检查并确保索引小于str.count

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

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

19 个答案:

答案 0 :(得分:549)

Alex's answer对这个问题提出了很好的建议和解决方案,但是,我偶然发现了一种更好的实现此功能的方法:

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

感谢Hamish提出the solution for Swift 3

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

答案 1 :(得分:51)

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

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

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

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

subscript

这意味着每次迭代数组时都需要解包一个可选项,或者使用已知索引执行其他任何操作,因为很少有人可能会访问越界索引。 Swift设计者在访问越界索引时以牺牲运行时异常为代价,选择了较少的可选解包。并且崩溃比由您在某个地方没有预料到的var fruits: [String] = ["Apple", "Banana", "Coconut"] var str: String = "I ate a \( fruits[0]! )" // ^ Added 引起的逻辑错误更可取。

我同意他们的观点。所以你不会改变默认的nil实现,因为你会破坏所有需要数组中非可选值的代码。

相反,您可以继承Array,并覆盖Array以返回可选项。或者,更实际地,您可以使用执行此操作的非下标方法扩展subscript

Array

Swift 3更新

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(index: Int) -> 需要替换为T? func get(index: Int) ->

答案 2 :(得分:12)

在Swift 2中有效

尽管已经有很多次回答了这个问题,但我想在Swift编程的流行方式中更加符合回答,用Crusty的话来说就是:“首先考虑protocol

•我们想做什么? - 仅在安全时获取Array给定索引的元素,否则nil •此功能应该基于什么实施? - Array subscript ing
•它从哪里获得此功能?
- struct Array模块中Swift的定义为 •没有更通用/抽象的东西? - 它采用protocol CollectionType来确保它
•没有更通用/抽象的东西? - 它还采用protocol Indexable ......
•是的,听起来像我们能做的最好。我们可以扩展它以获得我们想要的功能吗? - 但我们现在可以使用非常有限的类型(无Int)和属性(无count)!
•这就够了。 Swift的stdlib做得很好;)

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

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

答案 3 :(得分:8)

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

如果让index = array.checkIndexForSafety(index:Int)

  let item = array[safeIndex: index] 
     

如果让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
    }
}

答案 4 :(得分:7)

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

答案 5 :(得分:5)

extension Array {
    subscript (safe index: Index) -> Element? {
        return 0 <= index && index < count ? self[index] : nil
    }
}
  • O(1)表现
  • type safe
  • 正确处理[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?

答案 6 :(得分:5)

我意识到这是一个老问题。我现在正在使用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

对于那些争论使用nil的序列的人,您如何处理对于空集合返回nil的firstlast属性?

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

答案 7 :(得分:4)

我发现安全数组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)
            }
        }
    }
}

测试

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

答案 8 :(得分:1)

chainWebpack: config => {
    const files = fs.readdirSync('./src/assets/themes/');

    files.forEach(file => {
        config.module.rule('scss').oneOf('vue')
            .use('sass-vars-loader')
            .loader('@epegzz/sass-vars-loader')
            .options({
                syntax: 'scss',
                files: [
                    path.resolve(__dirname, `src/assets/themes/${file}`)
                ]
            })
    });
}

使用上面提到的扩展名,如果索引超出范围,则返回nil。

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

结果-无

答案 9 :(得分:1)

不确定为什么没有人提供一个扩展程序,该扩展程序还具有一个自动增长数组的设置器

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停止Swift本身就可以了。

答案 10 :(得分:1)

要传播操作失败的原因,错误比可选错误要好。下标不能引发错误,因此必须是一种方法。

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

答案 11 :(得分:1)

Swift列表中的“常见拒绝更改”包含对 changing 数组下标访问权限的提及,以返回可选而不是崩溃:

使Array<T>下标访问返回T?T!而不是T:当前数组的行为是intentional,因为它准确地反映出边界数组访问是逻辑错误。更改当前行为会使Array的访问速度减慢到无法接受的程度。该主题之前multiple次出现过,但极不可能被接受。

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

因此基本下标访问权限将不会更改以返回可选内容。

但是,Swift团队/社区似乎确实愿意向添加通过函数或下标向Arrays添加新的可选返回访问模式。

这已在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线程做出贡献的人。

答案 12 :(得分:1)

我在用例中用nil填充了数组:

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
}

同时检查Erica Sadun / Mike Ash的safe:标签的下标扩展名:http://ericasadun.com/2015/06/01/swift-safe-array-indexing-my-favorite-thing-of-the-new-week/

答案 13 :(得分:0)

我对数组做了简单的扩展

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, 

答案 14 :(得分:0)

我认为这不是一个好主意。建立不会导致尝试应用越界索引的可靠代码似乎更可取。

请考虑通过返回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")
}

答案 15 :(得分:0)

您可以尝试

if index >= 0 && index < array.count {
    print(array[index]) 
}

答案 16 :(得分:0)

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

答案 17 :(得分:-1)

迅速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>

但是由于集合是上下文相关的,所以我认为它是正确的。

答案 18 :(得分:-1)

当您只需要从数组中获取值并且您不介意对性能造成小的损失(即,如果您的集合不大)时,可以使用基于字典的替代方法不涉及(就我而言,太通用了)集合扩展:

// Assuming you have a collection named array:
let safeArray = Dictionary(uniqueKeysWithValues: zip(0..., array))
let value = safeArray[index] ?? defaultValue;