Ricerca di array sicuri (con limiti di verifica) in Swift, tramite collegamenti opzionali?

Se ho un array in Swift e provo ad accedere ad un indice che è fuori limite, c’è un errore di runtime non sorprendente:

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

Tuttavia, avrei pensato con tutta la catena opzionale e la sicurezza che Swift porta, sarebbe banale fare qualcosa del tipo:

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

Invece di:

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

Ma non è questo il caso – Devo usare l’istruzione ‘ if ‘ per controllare e garantire che l’indice sia inferiore a str.count .

Ho provato ad aggiungere la mia implementazione subscript() , ma non sono sicuro di come passare la chiamata all’implementazione originale o di accedere agli elementi (basati su indici) senza utilizzare la notazione degli indici:

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

La risposta di Alex ha buoni consigli e soluzioni per la domanda, tuttavia mi è capitato di imbattermi in un modo più bello di implementare questa funzionalità:

Swift 3.2 e successivi

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

Ringraziamo Hamish per aver trovato la soluzione per 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 } } 

Esempio

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

Se vuoi davvero questo comportamento, puzza come se tu volessi un dizionario invece di una matrice. I dizionari restituiscono nil quando si accede alle chiavi mancanti, il che ha senso perché è molto più difficile sapere se una chiave è presente in un dizionario poiché quelle chiavi possono essere qualsiasi cosa, dove in una matrice la chiave deve essere compresa in: 0 per count . Ed è incredibilmente comune iterare su questo intervallo, dove puoi essere assolutamente sicuro di avere un valore reale su ogni iterazione di un ciclo.

Penso che la ragione per cui non funziona in questo modo è una scelta progettuale fatta dagli sviluppatori Swift. Prendi il tuo esempio:

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

Se sai già che l’indice esiste, come nella maggior parte dei casi in cui usi un array, questo codice è ottimo. Tuttavia, se l’accesso a un pedice potrebbe restituire nil allora è stato modificato per restituire il tipo di metodo di subscript di Array come facoltativo. Questo cambia il tuo codice in:

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

Il che significa che è necessario scartare un opzionale ogni volta che viene ripetuto un array, o fare qualcos’altro con un indice noto, solo perché raramente si può accedere a un indice di fuori limite. I progettisti di Swift hanno optato per meno scartare gli optionals, a scapito di un’eccezione di runtime durante l’accesso agli indici di fuori limite. E un crash è preferibile a un errore logico causato da un nil che non ti aspetti nei tuoi dati da qualche parte.

E sono d’accordo con loro. Quindi non cambierai l’implementazione predefinita Array perché spezzerai tutto il codice che si aspetta un valore non opzionale dagli array.

Invece, è ansible Array sottoclass di Array e sovrascrivere l’ subscript per restituire un valore facoltativo. O, in pratica, puoi estendere Array con un metodo non-subscript che fa questo.

 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 } 

Aggiornamento Swift 3

func get(index: Int) -> T? deve essere sostituito da func get(index: Int) -> Element?

Valido in Swift 2

Anche se questo è già stato risposto molte volte, vorrei presentare una risposta più in linea su dove sta andando la moda della programmazione Swift, che nelle parole di Crusty¹ è: “Pensate prima al protocol

• Cosa vogliamo fare?
Ottieni un elemento di una Array dato un indice solo quando è sicuro e altrimenti nil
• A cosa dovrebbe basarsi questa funzionalità in base alla sua implementazione?
Array ing
• Da dove viene questa funzione?
La sua definizione di struct Array nel modulo Swift ce l’ha
• Niente di più generico / astratto?
Adotta il protocol CollectionType che lo garantisce
• Niente di più generico / astratto?
Adotta anche il protocol Indexable
• Sì, sembra il meglio che possiamo fare. Possiamo quindi estenderlo per avere questa funzionalità che vogliamo?
Ma abbiamo tipi molto limitati (no Int ) e proprietà (senza count ) con cui lavorare ora!
• Sarà abbastanza. Lo stdlib di Swift è fatto abbastanza bene;)

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

¹: non è vero, ma dà l’idea

  • Poiché gli array possono memorizzare valori nulli, non ha senso restituire un valore nullo se una chiamata di matrice [indice] è fuori limite.
  • Poiché non sappiamo come un utente vorrebbe gestire i problemi relativi ai limiti, non ha senso utilizzare operatori personalizzati.
  • Al contrario, utilizzare il stream di controllo tradizionale per gli oggetti non imballati e garantire la sicurezza del tipo.

se let indice = array.checkIndexForSafety (index: Int)

  let item = array[safeIndex: index] 

se let indice = 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 } } 

Ho trovato un array sicuro ottenere, impostare, inserire, rimuovere molto utile. Preferisco registrare e ignorare gli errori, poiché tutto il resto diventa difficile da gestire. Codice completo muggito

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

test

 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) prestazione
  • digita sicuro
  • si occupa correttamente di Optionals per [MyType?] (restituisce MyType ??, che può essere scartato su entrambi i livelli)
  • non porta a problemi per gli insiemi
  • codice conciso

Ecco alcuni test che ho eseguito per te:

 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? 

Per build la risposta di Nikita Kukushkin, a volte è necessario assegnare in modo sicuro agli indici di array e leggere da essi, cioè

 myArray[safe: badIndex] = newValue 

Ecco quindi un aggiornamento della risposta di Nikita (Swift 3.2) che consente anche di scrivere in modo sicuro su indici di array mutabili, aggiungendo il nome del parametro safe: parameter.

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

Ho riempito l’array con nil s nel mio caso d’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 } 

Controlla anche l’estensione del pedice con la safe: etichetta di Erica Sadun / Mike Ash: http://ericasadun.com/2015/06/01/swift-safe-array-indexing-my-favorite-thing-of-the-new- settimana/

Swift 4

Un’estensione per coloro che preferiscono una syntax più tradizionale:

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

Penso che questa non sia una buona idea. Sembra preferibile creare codice solido che non provochi l’applicazione di indici fuori limite.

Si prega di considerare che avere un errore di questo tipo fallisce silenziosamente (come suggerito dal vostro codice sopra) restituendo nil è incline a produrre errori ancora più complessi e più intrattabili.

Potresti fare il tuo override in un modo simile che hai usato e basta scrivere gli iscritti a modo tuo. L’unico inconveniente è che il codice esistente non sarà compatibile. Penso che trovare un aghook per scavalcare il generico x [i] (anche senza un preprocessore di testo come in C) sarà impegnativo.

Il più vicino a cui riesco a pensare è

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

EDIT : questo in realtà funziona. One-liner !!

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

Come catturare tale eccezione con indicizzazione errata:

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

Esempio:

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