¿Búsqueda de matriz segura (límites comprobados) en Swift, a través de enlaces opcionales?

Si tengo una matriz en Swift e bash acceder a un índice que está fuera de límites, hay un error de tiempo de ejecución no sorprendente:

var str = ["Apple", "Banana", "Coconut"] str[0] // "Apple" str[3] // EXC_BAD_INSTRUCTION 

Sin embargo, habría pensado que con todo el encadenamiento opcional y la seguridad que ofrece Swift, sería trivial hacer algo como:

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

En lugar de:

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

Pero este no es el caso. Tengo que usar la statement ol ‘ if para verificar y asegurarme de que el índice sea menor que la str.count .

Traté de agregar mi propia implementación de subscript() , pero no estoy seguro de cómo pasar la llamada a la implementación original o acceder a los elementos (basados ​​en índices) sin usar la notación de subíndices:

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

La respuesta de Alex tiene un buen consejo y una solución para la pregunta, sin embargo, he tropezado con una forma más agradable de implementar esta funcionalidad:

Swift 3.2 y más nuevos

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

Gracias a Hamish por idear la solución para Swift 3 .

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

Ejemplo

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

Si realmente quieres este comportamiento, huele como si quisieras un diccionario en lugar de una matriz. Los diccionarios devuelven nil al acceder a las teclas faltantes, lo que tiene sentido porque es mucho más difícil saber si una clave está presente en un diccionario, ya que esas claves pueden ser cualquier cosa, mientras que en una matriz la clave debe estar en un rango de 0 para count . Y es increíblemente común iterar en este rango, donde puede estar absolutamente seguro de tener un valor real en cada iteración de un ciclo.

Creo que la razón por la que no funciona de esta manera es una elección de diseño hecha por los desarrolladores de Swift. Toma tu ejemplo:

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

Si ya sabe que existe el índice, como lo hace en la mayoría de los casos en que utiliza una matriz, este código es excelente. Sin embargo, si acceder a un subíndice podría devolver nil entonces ha cambiado para devolver el tipo del método de subscript de Array para que sea opcional. Esto cambia tu código a:

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

Lo que significa que necesitaría desenvolver un elemento opcional cada vez que iterara a través de una matriz, o hiciera cualquier otra cosa con un índice conocido, simplemente porque rara vez podría acceder a un índice fuera de límites. Los diseñadores de Swift optaron por un menor desenvolvimiento de los opcionales, a expensas de una excepción de tiempo de ejecución al acceder a índices fuera de límites. Y un locking es preferible a un error de lógica causado por un nil que no esperaba en sus datos en alguna parte.

Y estoy de acuerdo con ellos. Por lo tanto, no cambiará la implementación predeterminada de Array porque rompería todo el código que espera valores no opcionales de las matrices.

En su lugar, podría subclase Array y anular el subscript para devolver un opcional. O, de forma más práctica, podría extender Array con un método que no sea un subíndice que hace esto.

 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 } 

Actualización de Swift 3

func get(index: Int) -> T? necesita ser reemplazado por func get(index: Int) -> Element?

Válido en Swift 2

Aunque esto ya se ha respondido muchas veces, me gustaría presentar una respuesta más en línea sobre dónde va la moda de la progtwigción Swift, que en palabras de Crusty¹ es: “Pensar primero en el protocol

• ¿Qué queremos hacer?
Obtener un elemento de una Array dado un índice solo cuando es seguro, y nada de lo contrario
• ¿En qué debería basarse esta funcionalidad para su implementación?
Array subscript ing
• ¿De dónde saca esta característica?
Su definición de struct Array en el módulo Swift tiene
• ¿Nada más genérico / abstracto?
Adopta el protocol CollectionType que lo asegura también
• ¿Nada más genérico / abstracto?
Adopta protocol Indexable también …
• Sí, suena como lo mejor que podemos hacer. ¿Podemos ampliarlo para tener esta característica que queremos?
Pero tenemos tipos muy limitados (sin Int ) y propiedades (sin count ) para trabajar ahora!
• Será suficiente. Swift’s stdlib está hecho bastante bien;)

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

¹: no es cierto, pero da la idea

  • Debido a que las matrices pueden almacenar valores nulos, no tiene sentido devolver un valor nulo si una llamada de matriz [índice] está fuera de límites.
  • Debido a que no sabemos cómo un usuario le gustaría manejar problemas fuera de límites, no tiene sentido usar operadores personalizados.
  • Por el contrario, utilice el flujo de control tradicional para desenvolver objetos y garantizar la seguridad del tipo.

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

  let item = array[safeIndex: index] 

if let index = array.checkIndexForSafety (índice: 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 } } 

Encontré la matriz segura get, set, insert, remove muy útil. Prefiero iniciar sesión e ignorar los errores ya que todo lo demás pronto se vuelve difícil de administrar. Código completo abajo

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

Pruebas

 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]) } } 
 extension Array { subscript (safe index: Index) -> Element? { return 0 < = index && index < count ? self[index] : nil } } 
  • O (1) rendimiento
  • tipo seguro
  • trata correctamente con Opcionales para [MyType?] (devuelve MyType ??, que se puede desenvolver en ambos niveles)
  • no conduce a problemas para Sets
  • código conciso

Aquí hay algunas pruebas que ejecuté para ti:

 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? 

Para construir sobre la respuesta de Nikita Kukushkin, a veces es necesario asignar de forma segura a los índices de matriz, así como leer de ellos, es decir,

 myArray[safe: badIndex] = newValue 

Así que aquí hay una actualización de la respuesta de Nikita (Swift 3.2) que también permite escribir de forma segura a los índices de matrices mutables, agregando el nombre seguro: parámetro.

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

He rellenado la matriz con nil en mi caso de uso:

 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 } 

También verifique la extensión del subíndice con safe: label por Erica Sadun / Mike Ash: http://ericasadun.com/2015/06/01/swift-safe-array-indexing-my-favorite-thing-of-the-new- semana/

Swift 4

Una extensión para aquellos que prefieren una syntax más tradicional:

 extension Array where Element: Equatable { func object(at index: Int) -> Element? { return indices.contains(index) ? self[index] : nil } } 

Creo que esta no es una buena idea. Parece preferible crear código sólido que no dé como resultado tratar de aplicar índices fuera de límites.

Tenga en cuenta que si un error de este tipo falla de forma silenciosa (como lo sugiere su código anterior) al devolver nil es propenso a producir errores aún más complejos e irresolubles.

Puede hacer su anulación de manera similar a la que utilizó y simplemente escribir los subíndices a su manera. El único inconveniente es que el código existente no será compatible. Creo que encontrar un gancho para anular el genérico x [i] (también sin un preprocesador de texto como en C) será un desafío.

Lo más cercano que puedo pensar es

 // compile error: if theIndex < str.count && let existing = str[theIndex] 

EDITAR : Esto realmente funciona. ¡¡Un trazador de líneas!!

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

Cómo atrapar una excepción con indexación incorrecta:

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

Ejemplo:

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