通过可选绑定在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在(1,2,7)中...或者X在myArray中
Maury Markowitz

Answers:


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

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

let array = [1, 2, 3]

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

45
我认为这绝对值得关注-不错的工作。我喜欢包含的safe:参数名称以确保两者的区别。
Craig Otis 2015年

11
自Swift 2(Xcode 7)起,需要进行一些调整:return self.indices ~= index ? self[index] : nil;
Tim

7
关于Swift 3版本:可能只是提示情况,但还是提示:在某些情况下,上面的“安全”下标版本不安全(而Swift 2版本是):对于Collection类型Indices为不连续。例如,Set例如,如果我们要通过索引(SetIndex<Element>)访问set元素,我们可能会遇到索引为>= startIndex和的运行时异常< 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

如果您确实想要此行为,则闻起来就像您要使用Dictionary而不是Array。字典nil在访问丢失的键时返回,这是有道理的,因为很难知道字典中是否存在键,因为这些键可以是任何东西,键在数组中必须在以下范围内:0to 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
+1(和接受)提到将返回类型更改为subscript()可选的问题-这是覆盖默认行为时面临的主要障碍。(实际上我根本无法使它正常工作)我避免编写get()扩展方法,这在其他情况下(Obj-C类别,有人吗?)get(是显而易见的选择,但它没有比更大[,并且使其扩展很明显,该行为可能与其他开发人员可能期望的Swift下标运算符有所不同。谢谢!
Craig Otis 2014年

3
为了使它更短,我使用at();)谢谢!
hyouuu

7
从Swift 2.0 T开始,已重命名为Element。谨在此提醒您:)
Stas Zhukovskiy 2015年

3
在此讨论中,为什么不将边界检查引入Swift来返回可选值是另一个原因,因为返回nil而不是导致超出范围索引的异常是模棱两可的。由于eg Array<String?>也可能作为集合的有效成员返回nil,因此您将无法区分这两种情况。如果您拥有自己的集合类型,并且知道它永远不会返回nil值(也就是应用程序的上下文),那么您可以扩展Swift进行安全边界检查,如本文所述。
亚伦

做工

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
被低估的答案!这是正确的方法!
Reid

14

在Swift 2中有效

尽管已经多次回答了这个问题,但我还是想更加顺应Swift编程的发展趋势提出一个答案,用Crusty的话¹表示:“ protocol首先思考”。

•我们想做什么?
- 仅在安全时获取Array给定索引的元素,nil否则
•获得此功能的实现基础?
- Array subscript荷兰国际集团
•哪里不从得到这个功能吗?
- 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新手,我不明白这个答案。最后的代码代表什么?那是一个解决方案,如果是的话,我该如何实际使用它?
托马斯·坦佩尔曼'17

3
抱歉,这个答案对Swift 3不再有效,但是过程肯定是正确的。唯一的区别是,现在您Collection可能应该停止在:)
DeFrenZ

11
extension Array {
    subscript (safe index: Index) -> Element? {
        return 0 <= index && index < count ? self[index] : nil
    }
}
  • 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?

10
  • 因为数组可能存储nil值,所以如果array [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
    }
}

1
有趣的方法。任何原因SafeIndex是类而不是结构?
Stef

8

斯威夫特4

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

extension Array {

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

您不需要将数组元素约束为等于即可检查索引是否包含其索引。
Leo Dabus

是的-很好-仅需要其他安全方法(例如deleteObject等)才可以使用。–
Matjan

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

测验

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

结果-无


3

我意识到这是一个老问题。我现在正在使用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的序列的人,您如何处理firstlast属性为空集合返回nil?

我之所以喜欢它,是因为我可以抓住现有的东西,并用它来获得想要的结果。我也知道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在这种情况下使用a 不会使程序更复杂,也不会使错误更棘手。它只是将传统的两语句if边界检查和实际查找浓缩为一个单行的压缩语句。在某些情况下(尤其是在UI中工作),索引超出范围是正常的,例如要求NSTableView提供selectedRow不带选择的。
Craig Otis 2014年

3
@Mundi这似乎是一条评论,而不是对OP问题的答案。
jlehr 2014年

1
@CraigOtis不确定我是否同意。您可以在“单行的简明语句”中简洁地编写此检查,例如,使用countElements或像OP一样使用count,而不是该语言定义编写数组下标的方式。
芒迪2014年

1
@jlehr也许不是。质疑所提出问题的意图或智慧是公平的游戏。
芒迪

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列表的“ Commall Rejected Changes”包含更改 Array下标访问以返回可选而不是崩溃的提示:

使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;
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.