web-dev-qa-db-fra.com

Recherche de tableau sécurisée (corrigée) dans Swift, via des liaisons facultatives?

Si j'ai un tableau dans Swift et que j'essaie d'accéder à un index qui est hors limites, il y a une erreur d'exécution imprévue:

var str = ["Apple", "Banana", "Coconut"]

str[0] // "Apple"
str[3] // EXC_BAD_INSTRUCTION

Cependant, j'aurais pensé avec tous les chaînages optionnels et sécurité que Swift apporte, il serait trivial de faire quelque chose comme:

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

Au lieu de:

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

Mais ce n'est pas le cas - je dois utiliser l'instruction ol 'if pour vérifier et vérifier que l'index est inférieur à str.count.

J'ai essayé d'ajouter ma propre implémentation subscript(), mais je ne sais pas comment passer l'appel à l'implémentation d'origine ni accéder aux éléments (à base d'index) sans utiliser la notation en indice:

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

La réponse d'Alex a de bons conseils et une bonne solution à la question. Cependant, je suis tombé par hasard sur un moyen plus agréable d'implémenter cette fonctionnalité:

Swift 3.2 et plus récent

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

Nous remercions Hamish d’avoir proposé la solution pour Swift .

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

Exemple

let array = [1, 2, 3]

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

Si vous voulez vraiment ce comportement, ça sent comme si vous voulez un dictionnaire au lieu d'un tableau. Les dictionnaires renvoient nil lors de l’accès aux clés manquantes, ce qui est logique car il est beaucoup plus difficile de savoir si une clé est présente dans un dictionnaire car ces clés peuvent être n'importe quoi, où dans un tableau la clé doit dans une plage de: _0_ à count. Et il est incroyablement courant d'itérer sur cette plage, où vous pouvez être absolument certain d'avoir une valeur réelle à chaque itération d'une boucle.

Je pense que la raison pour laquelle cela ne fonctionne pas de cette façon est un choix de conception effectué par les développeurs Swift. Prenez votre exemple:

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

Si vous savez déjà que l'index existe, comme dans la plupart des cas où vous utilisez un tableau, ce code est génial. Cependant, si l'accès à un indice pouvait éventuellement renvoyer nil, vous avez alors modifié le type de retour de Array 'subscript méthode soit facultative. Cela change votre code à:

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

Ce qui signifie que vous devez dérouler une option chaque fois que vous parcourez un tableau ou faites autre chose avec un index connu, simplement parce que vous pourriez rarement accéder à un index hors limites. Les concepteurs Swift ont opté pour une réduction moins importante du nombre d'options, au détriment d'une exception d'exécution, lors de l'accès à des index hors limites. Et un crash est préférable à une erreur de logique provoquée par un nil auquel vous ne vous attendiez pas quelque part dans vos données.

Et je suis d'accord avec eux. Par conséquent, vous ne modifierez pas l'implémentation Array par défaut, car vous risqueriez de casser tout le code qui attend des valeurs non facultatives des tableaux.

Au lieu de cela, vous pouvez sous-classer Array et remplacer subscript pour renvoyer une valeur facultative. Ou, plus concrètement, vous pouvez étendre Array avec une méthode sans indice qui le fait.

_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 Mise à jour

func get(index: Int) ->T? doit être remplacé par func get(index: Int) ->Element?

56
Alex Wayne

Valable dans Swift 2

Même si cela a déjà été répondu à maintes reprises, je voudrais présenter une réponse plus conforme à la tendance actuelle de la programmation de Swift, qui dans les termes de Crusty¹ est: "Pensez protocols en premier "

• Que voulons-nous faire?
- Obtenir un élément d'un Array donné un index uniquement lorsqu'il est sûr, et nil sinon
• Sur quoi cette fonctionnalité devrait-elle être basée?
- Arraysubscripting
• D'où vient cette fonctionnalité?
- Sa définition de struct Array dans le module Swift l'a
• Rien de plus générique/abstrait?
- Il adopte protocol CollectionType qui l’assure également
• Rien de plus générique/abstrait?
- Il adopte protocol Indexable aussi ...
• Oui, cela semble être le meilleur que nous puissions faire. Pouvons-nous ensuite l'étendre pour avoir cette fonctionnalité que nous voulons?
- Mais nous avons des types très limités (pas de Int) et des propriétés (pas de count) pour travailler maintenant!
• Ce sera suffisant. La stdlib de Swift est assez bien faite;)

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

¹: pas vrai, mais ça donne une idée

14
DeFrenZ
  • Étant donné que les tableaux peuvent stocker des valeurs nulles, il est inutile de renvoyer un zéro si un appel de tableau [index] est hors limites.
  • Étant donné que nous ne savons pas comment un utilisateur souhaite gérer des problèmes hors limites, il n’a aucun sens de faire appel à des opérateurs personnalisés.
  • En revanche, utilisez le flux de contrôle traditionnel pour déplier des objets et assurez la sécurité du type.

si let index = array.checkIndexForSafety (index: Int)

  let item = array[safeIndex: index] 

si 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
    }
}
10
M.S. Cline

Pour donner suite à la réponse de Nikita Kukushkin, vous devez parfois affecter en toute sécurité des index de tableaux ainsi que les lire, c.-à-d.

myArray[safe: badIndex] = newValue

Voici donc une mise à jour de la réponse de Nikita (Swift 3.2) qui permet également d’écrire en toute sécurité dans des index de tableaux mutables, en ajoutant le paramètre safe: nom du paramètre.

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

Swift 4

Une extension pour ceux qui préfèrent une syntaxe plus traditionnelle:

extension Array {

    func item(at index: Int) -> Element? {
        return indices.contains(index) ? self[index] : nil
    }
}
7
Matjan
extension Array {
    subscript (safe index: Index) -> Element? {
        return 0 <= index && index < count ? self[index] : nil
    }
}
  • O (1) performance
  • tapez safe
  • gère correctement les options pour [MyType?] (retourne MyType ??, qui peut être décompressé aux deux niveaux)
  • ne pose pas de problèmes pour les sets
  • code concis

Voici quelques tests que j'ai courus pour vous:

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?
6
thetrutz

J'ai trouvé un tableau sécurisé get, set, insert, remove très utile. Je préfère me connecter et ignorer les erreurs car tout le reste devient vite difficile à gérer. Code complet ci-dessous

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

Des tests

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])
    }
}
5
Ivan Rep
extension Array {
  subscript (safe index: UInt) -> Element? {
    return Int(index) < count ? self[Int(index)] : nil
  }
}

Utiliser l'extension mentionnée ci-dessus renvoie nil si l'index est dépassé à tout moment.

let fruits = ["Apple","banana"]
print("result-\(fruits[safe : 2])")

résultat - nul

2
Vinayak Pal

Je pense que ce n'est pas une bonne idée. Il semble préférable de créer un code solide ne conduisant pas à appliquer des index hors limites.

Veuillez considérer qu’une telle erreur échoue de manière silencieuse (comme le suggère votre code ci-dessus) en renvoyant nil risque de produire des erreurs encore plus complexes et plus difficiles à traiter.

Vous pouvez effectuer votre remplacement de la même manière que vous avez utilisé et écrire les indices à votre guise. Le seul inconvénient est que le code existant ne sera pas compatible. Je pense que trouver un crochet pour remplacer le x [i] générique (également sans préprocesseur de texte comme en C) sera un défi.

Le plus proche que je peux penser est

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

EDIT: Cela fonctionne réellement. Bon mot!!

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

J'ai rempli le tableau avec nils dans mon cas d'utilisation:

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
}

Vérifiez également l'extension avec l'indice safe: par Erica Sadun/Mike Ash: http://ericasadun.com/2015/06/01/Swift-safe-array-indexing-my-favorite-thing -de-la-nouvelle-semaine /

1
Marián Černý

J'ai fait une simple extension pour array

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

}

cela fonctionne parfaitement comme prévu

Exemple

   if let firstElemntToLoad = roots.iof(0)?.children?.iof(0)?.cNode, 
0
Mohamed Deux