如果我在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。

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

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

当前回答

当你只需要从一个数组中获取值,并且你不介意一个小的性能损失(即如果你的集合不是很大),有一个基于dictionary的替代方案,它不涉及(对我来说太通用了)集合扩展:

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

其他回答

当你只需要从一个数组中获取值,并且你不介意一个小的性能损失(即如果你的集合不是很大),有一个基于dictionary的替代方案,它不涉及(对我来说太通用了)集合扩展:

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

在我的用例中,我用nils填充了数组:

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/

我认为这不是一个好主意。似乎更可取的做法是构建稳定的代码,不导致试图应用越界索引。

请考虑让这样的错误通过返回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")
}

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

使数组<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

值得注意的是,克里斯·拉特纳给这个想法打了一个“+1”:

同意,最常被建议的拼写是:yourArray[safe: idx],这对我来说似乎很棒。加上这个,我是+1。 https://forums.swift.org/t/add-accessor-with-bounds-check-to-array/16871/13

因此,在Swift的未来版本中,这可能是开箱即用的。我鼓励任何想要它的人加入Swift Evolution线程。

我发现安全数组获取,设置,插入,删除非常有用。我更倾向于记录并忽略错误,因为所有其他错误很快就会变得难以管理。完整代码如下

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