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

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

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

Alex的回答对于这个问题有很好的build议和解决scheme,但是我偶然发现了一个实现这个function的更好的方法:

Swift 3.2和4

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

Swift 3.0和3.1

 extension Collection where Indices.Iterator.Element == Index { /// Returns the element at the specified index iff it is within bounds, otherwise nil. subscript (safe index: Index) -> Generator.Element? { return indices.contains(index) ? self[index] : nil } } 

感谢Hamish 提出Swift 3的解决scheme 。

Swift 2

 extension CollectionType { /// Returns the element at the specified index iff 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) } } 

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

我认为它不这样工作的原因是由Swift开发人员做出的一个deviseselect。 举个例子:

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

如果您已经知道索引存在,就像您在大多数情况下使用数组一样,此代码非常好。 但是,如果访问下标可能返回nil那么你已经改变了返回 Arraysubscript方法的types是一个可选项。 这将您的代码更改为:

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

这意味着每次迭代数组时,您都需要打开可选项,或者使用已知索引进行其他操作,这是因为很less有人可能访问出界索引。 Swiftdevise者select了较less的可选项,而在访问超出界限索引时则牺牲了运行时exception。 而崩溃是更好的一个逻辑错误引起的一个nil你没有想到在你的数据的地方。

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

相反,你可以inheritanceArray ,重写下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) ->取代func get(index: Int) -> Element?

在Swift 2中有效

尽pipe这已经被很多次的回答了,但是我想提出一个更加符合Swift编程风格的答案,Crusty的话是:“首先思考protocol

•我们想做什么?
只有在安全的情况下才能获得给定索引的Array元素,否则nil
这个function基于什么实现?
Array subscript
•它从哪里获得此function?
它在Swift模块中的struct Array定义有它
•没有更通用的/抽象的?
它采用protocol CollectionType来保证它
•没有更通用的/抽象的?
它采用protocol Indexable以及…
是的,听起来是我们能做的最好的。 那么我们可以扩展它来拥有我们想要的这个function吗?
但是我们有非常有限的types(没有Int )和属性(不count )现在可以使用!
•这将是足够的。 Swift的stdlib做得很好;)

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

¹:不正确,但它提供了这个想法

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

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

  let item = array[safeIndex: index] 

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

我在我的用例中填充了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-周/

我发现安全的数组获取,设置,插入,删除非常有用。 我更喜欢logging和忽略错误,因为所有其他的很快就难以pipe理。 完整的代码波纹pipe

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

testing

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

我认为这不是一个好主意。 构build可靠的代码似乎最好不要导致尝试应用越界索引。

请认为,如果错误地失败(如上面的代码所示),返回nil容易产生更复杂,更难处理的错误。

你可以用你用过的类似的方式做你的覆写,只用你自己的方式写下标。 唯一的缺点是现有的代码将不兼容。 我认为find一个钩子来覆盖通用的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") } 
 extension Array { subscript (safe index: Index) -> Element? { return 0 <= index && index < count ? self[index] : nil } } 
  • O(1)的performance
  • types安全
  • 正确地处理[MyType?]的可选项(返回MyType ??,可以在两个级别上展开)
  • 不会导致集合的问题
  • 简洁的代码

以下是我为你跑的一些testing:

 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? 

如何用错误的索引来捕获这样的exception:

 extension Array { func lookup(index : UInt) throws -> Element { if Int(index) >= count { throw NSError( domain: "com.sadun", code: 0, userInfo: [NSLocalizedFailureReasonErrorKey: "Array index out of bounds"] ) } return self[Int(index)] } } 

例:

 do { try ["Apple", "Banana", "Coconut"].lookup(index: 3) } catch { print(error) }