web-dev-qa-db-fra.com

Création d'une extension pour filtrer les nils à partir d'un tableau dans Swift

J'essaie d'écrire une extension dans Array qui permettra de transformer un tableau de T facultatifs en un tableau de T non facultatifs.

par exemple. cela pourrait être écrit comme une fonction libre comme ceci:

func removeAllNils(array: [T?]) -> [T] {
    return array
        .filter({ $0 != nil })   // remove nils, still a [T?]
        .map({ $0! })            // convert each element from a T? to a T
}

Mais, je ne peux pas faire fonctionner cela comme une extension. J'essaie de dire au compilateur que l'extension ne s'applique qu'aux tableaux de valeurs facultatives. Voici ce que j'ai jusqu'à présent:

extension Array {
    func filterNils<U, T: Optional<U>>() -> [U] {
        return filter({ $0 != nil }).map({ $0! })
    }
}

(il ne compile pas!)

46
Javawag

Il n'est pas possible de restreindre le type défini pour une structure ou une classe générique - le tableau est conçu pour fonctionner avec n'importe quel type, vous ne pouvez donc pas ajouter une méthode qui fonctionne pour un sous-ensemble de types. Les contraintes de type ne peuvent être spécifiées que lors de la déclaration du type générique

La seule façon d'atteindre ce dont vous avez besoin est de créer soit une fonction globale soit une méthode statique - dans ce dernier cas:

extension Array {
    static func filterNils(array: [T?]) -> [T] {
        return array.filter { $0 != nil }.map { $0! }
    }
}

var array:[Int?] = [1, nil, 2, 3, nil]

Array.filterNils(array)

Ou utilisez simplement compactMap (auparavant flatMap), qui peut être utilisé pour supprimer toutes les valeurs nulles:

[1, 2, nil, 4].compactMap { $0 } // Returns [1, 2, 4]
42
Antonio

Depuis Swift 2.0, vous n'avez pas besoin d'écrire votre propre extension pour filtrer les valeurs nulles d'un tableau, vous pouvez utiliser flatMap, qui aplatit le tableau et filtre les nils:

let optionals : [String?] = ["a", "b", nil, "d"]
let nonOptionals = optionals.flatMap{$0}
print(nonOptionals)

Tirages:

[a, b, d]

Remarque:

Il existe 2 flatMap fonctions:

80
Chris Trevarthen

TL; DR

Swift 4

Utilisez array.compactMap { $0 }. Apple a mis à jour le framework pour qu'il ne provoque plus de bugs/confusion.

Swift 3

Pour éviter les bugs/confusion potentiels, n'utilisez pas array.flatMap { $0 } Pour supprimer nils; utilisez une méthode d'extension telle que array.removeNils() à la place (implémentation ci-dessous, mise à jour pour Swift 3.0 ) .


Bien que array.flatMap { $0 } Fonctionne la plupart du temps, il existe plusieurs raisons de privilégier une extension array.removeNils():

  • removeNils décrit exactement ce que vous voulez faire : supprimez les valeurs de nil. Quelqu'un qui n'est pas familier avec flatMap devrait le rechercher et, quand il le cherchera, s'il y prête une grande attention, il arrivera à la même conclusion que mon prochain point;
  • flatMap a deux implémentations différentes qui font deux choses entièrement différentes . Basé sur la vérification de type, le compilateur va décider lequel est invoqué. Cela peut être très problématique dans Swift, car l'inférence de type est largement utilisée. (Par exemple, pour déterminer le type réel d'une variable, vous devrez peut-être inspecter plusieurs fichiers.) Un refactoriseur pourrait amener votre application à appeler la mauvaise version de flatMap, ce qui pourrait conduire à bogues difficiles à trouver .
  • Puisqu'il y a deux fonctions complètement différentes, cela rend la compréhension de flatMap beaucoup plus difficile puisque vous pouvez confondre facilement les deux.
  • flatMap peut être appelé sur des tableaux non optionnels (par exemple [Int]), Donc si vous refactorisez un tableau de [Int?] À [Int] Vous pouvez laisse accidentellement des appels à flatMap { $0 } dont le compilateur ne vous avertira pas. Au mieux, il se renverra simplement, au pire, il provoquera l'exécution de l'autre implémentation, ce qui pourrait entraîner des bogues.
  • Dans Swift 3, si vous ne convertissez pas explicitement le type de retour, le compilateur choisira la mauvaise version , ce qui entraîne des conséquences inattendues. (Voir Swift 3 section ci-dessous)
  • Enfin, cela ralentit le compilateur car le système de vérification de type doit déterminer laquelle des fonctions surchargées appeler.

Pour récapituler, il existe deux versions de la fonction en question, toutes deux, malheureusement, nommées flatMap.

  1. Aplatir les séquences en supprimant un niveau d'imbrication (par exemple [[1, 2], [3]] -> [1, 2, 3])

    public struct Array<Element> : RandomAccessCollection, MutableCollection {
        /// Returns an array containing the concatenated results of calling the
        /// given transformation with each element of this sequence.
        ///
        /// Use this method to receive a single-level collection when your
        /// transformation produces a sequence or collection for each element.
        ///
        /// In this example, note the difference in the result of using `map` and
        /// `flatMap` with a transformation that returns an array.
        ///
        ///     let numbers = [1, 2, 3, 4]
        ///
        ///     let mapped = numbers.map { Array(count: $0, repeatedValue: $0) }
        ///     // [[1], [2, 2], [3, 3, 3], [4, 4, 4, 4]]
        ///
        ///     let flatMapped = numbers.flatMap { Array(count: $0, repeatedValue: $0) }
        ///     // [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
        ///
        /// In fact, `s.flatMap(transform)`  is equivalent to
        /// `Array(s.map(transform).joined())`.
        ///
        /// - Parameter transform: A closure that accepts an element of this
        ///   sequence as its argument and returns a sequence or collection.
        /// - Returns: The resulting flattened array.
        ///
        /// - Complexity: O(*m* + *n*), where *m* is the length of this sequence
        ///   and *n* is the length of the result.
        /// - SeeAlso: `joined()`, `map(_:)`
        public func flatMap<SegmentOfResult : Sequence>(_ transform: (Element) throws -> SegmentOfResult) rethrows -> [SegmentOfResult.Iterator.Element]
    }
    
  2. Supprimer des éléments d'une séquence (par exemple [1, nil, 3] -> [1, 3])

    public struct Array<Element> : RandomAccessCollection, MutableCollection {
        /// Returns an array containing the non-`nil` results of calling the given
        /// transformation with each element of this sequence.
        ///
        /// Use this method to receive an array of nonoptional values when your
        /// transformation produces an optional value.
        ///
        /// In this example, note the difference in the result of using `map` and
        /// `flatMap` with a transformation that returns an optional `Int` value.
        ///
        ///     let possibleNumbers = ["1", "2", "three", "///4///", "5"]
        ///
        ///     let mapped: [Int?] = numbers.map { str in Int(str) }
        ///     // [1, 2, nil, nil, 5]
        ///
        ///     let flatMapped: [Int] = numbers.flatMap { str in Int(str) }
        ///     // [1, 2, 5]
        ///
        /// - Parameter transform: A closure that accepts an element of this
        ///   sequence as its argument and returns an optional value.
        /// - Returns: An array of the non-`nil` results of calling `transform`
        ///   with each element of the sequence.
        ///
        /// - Complexity: O(*m* + *n*), where *m* is the length of this sequence
        ///   and *n* is the length of the result.
        public func flatMap<ElementOfResult>(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult]
    }
    

# 2 est celui que les gens utilisent pour supprimer les nils en passant { $0 } Comme transform. Cela fonctionne car la méthode effectue une mappe, puis filtre tous les éléments nil.

Vous vous demandez peut-être "Pourquoi est-ce que Apple n'a pas renommé # 2 en removeNils()"? One chose à garder à l'esprit est que l'utilisation de flatMap pour supprimer nils n'est pas la seule utilisation de # 2. En fait, puisque les deux versions prennent en charge une fonction transform, elles peuvent être beaucoup plus puissantes que ces exemples ci-dessus.

Par exemple, # 1 pourrait facilement diviser un tableau de chaînes en caractères individuels (aplatir) et mettre en majuscule chaque lettre (carte):

["abc", "d"].flatMap { $0.uppercaseString.characters } == ["A", "B", "C", "D"]

Alors que le numéro 2 pourrait facilement supprimer tous les nombres pairs (aplatir) et multiplier chaque nombre par -1 (Carte):

[1, 2, 3, 4, 5, 6].flatMap { ($0 % 2 == 0) ? nil : -$0 } == [-1, -3, -5]

(Notez que ce dernier exemple peut faire tourner Xcode 7.3 pendant très longtemps car aucun type explicite n'est indiqué. Une preuve supplémentaire de la raison pour laquelle les méthodes devraient avoir des noms différents.)

Le vrai danger d'utiliser aveuglément flatMap { $0 } Pour supprimer nil ne vient pas lorsque vous l'appelez sur [1, 2], Mais plutôt lorsque vous l'appelez sur quelque chose comme [[1], [2]] . Dans le premier cas, il appellera sans risque l'invocation # 2 et renverra [1, 2]. Dans ce dernier cas, vous pouvez penser qu'il ferait la même chose (retourne inoffensivement [[1], [2]] Car il n'y a pas de valeurs nil), mais il retournera en fait [1, 2] Car il utilise l'invocation #1.

Le fait que flatMap { $0 } Soit utilisé pour supprimer nil semble être davantage de Swift communautérecommandation = plutôt que celui d'Apple. Peut-être que si Apple remarque cette tendance, ils fourniront éventuellement une fonction removeNils() ou quelque chose de similaire.

Jusque-là, il nous reste à trouver notre propre solution.


Solution

// Updated for Swift 3.0
protocol OptionalType {
    associatedtype Wrapped
    func map<U>(_ f: (Wrapped) throws -> U) rethrows -> U?
}

extension Optional: OptionalType {}

extension Sequence where Iterator.Element: OptionalType {
    func removeNils() -> [Iterator.Element.Wrapped] {
        var result: [Iterator.Element.Wrapped] = []
        for element in self {
            if let element = element.map({ $0 }) {
                result.append(element)
            }
        }
        return result
    }
}

(Remarque: Ne vous confondez pas avec element.map ... cela n'a rien à voir avec le flatMap discuté dans cet article. Il utilise Optional) map function pour obtenir un type facultatif qui peut être déballé. Si vous omettez cette partie, vous obtiendrez cette erreur de syntaxe: "erreur: l'initialiseur pour la liaison conditionnelle doit avoir le type facultatif, pas 'Self. Generator.Element '. "Pour plus d'informations sur la façon dont map() nous aide, voir cette réponse que j'ai écrite à propos de l'ajout d'une méthode d'extension sur SequenceType pour compter les non-nils .)

Usage

let a: [Int?] = [1, nil, 3]
a.removeNils() == [1, 3]

Exemple

var myArray: [Int?] = [1, nil, 2]
assert(myArray.flatMap { $0 } == [1, 2], "Flat map works great when it's acting on an array of optionals.")
assert(myArray.removeNils() == [1, 2])

var myOtherArray: [Int] = [1, 2]
assert(myOtherArray.flatMap { $0 } == [1, 2], "However, it can still be invoked on non-optional arrays.")
assert(myOtherArray.removeNils() == [1, 2]) // syntax error: type 'Int' does not conform to protocol 'OptionalType'

var myBenignArray: [[Int]?] = [[1], [2, 3], [4]]
assert(myBenignArray.flatMap { $0 } == [[1], [2, 3], [4]], "Which can be dangerous when used on nested SequenceTypes such as arrays.")
assert(myBenignArray.removeNils() == [[1], [2, 3], [4]])

var myDangerousArray: [[Int]] = [[1], [2, 3], [4]]
assert(myDangerousArray.flatMap { $0 } == [1, 2, 3, 4], "If you forget a single '?' from the type, you'll get a completely different function invocation.")
assert(myDangerousArray.removeNils() == [[1], [2, 3], [4]]) // syntax error: type '[Int]' does not conform to protocol 'OptionalType'

(Remarquez que sur le dernier, flatMap renvoie [1, 2, 3, 4] Alors que removeNils () aurait dû retourner [[1], [2, 3], [4]].)


La solution est similaire à la réponse @fabb liée à.

Cependant, j'ai fait quelques modifications:

  • Je n'ai pas nommé la méthode flatten, car il existe déjà une méthode flatten pour les types de séquence, et donner le même nom à des méthodes entièrement différentes est ce qui nous a mis dans ce pétrin en premier lieu . Sans oublier qu'il est beaucoup plus facile de mal interpréter ce que fait flatten que ce n'est removeNils.
  • Plutôt que de créer un nouveau type T sur OptionalType, il utilise le même nom que Optional (Wrapped).
  • Au lieu de exécuter map{}.filter{}.map{} , ce qui conduit à O(M + N) fois, je fais une boucle dans le tableau une fois.
  • Au lieu d'utiliser flatMap pour passer de Generator.Element À Generator.Element.Wrapped?, J'utilise map. Il n'est pas nécessaire de renvoyer des valeurs de nil à l'intérieur de la fonction map, donc map suffira. En évitant la fonction flatMap, il est plus difficile de confondre une autre méthode (c.-à-d. 3e) avec le même nom qui a une fonction entièrement différente.

Le seul inconvénient de l'utilisation de removeNils par rapport à flatMap est que le vérificateur de type peut avoir besoin d'un peu plus d'indices:

[1, nil, 3].flatMap { $0 } // works
[1, nil, 3].removeNils() // syntax error: type of expression is ambiguous without more context

// but it's not all bad, since flatMap can have similar problems when a variable is used:
let a = [1, nil, 3] // syntax error: type of expression is ambiguous without more context
a.flatMap { $0 }
a.removeNils()

Je n'y ai pas beaucoup réfléchi, mais il semble que vous puissiez ajouter:

extension SequenceType {
  func removeNils() -> Self {
    return self
  }
}

si vous souhaitez pouvoir appeler la méthode sur des tableaux contenant des éléments non facultatifs. Cela pourrait faciliter un renommage massif (par exemple flatMap { $0 } -> removeNils()) plus facile.


S'affecter à soi est différent de l'affecter à une nouvelle variable?!

Jetez un œil au code suivant:

var a: [String?] = [nil, nil]

var b = a.flatMap{$0}
b // == []

a = a.flatMap{$0}
a // == [nil, nil]

Étonnamment, a = a.flatMap { $0 } Ne supprime pas les nils lorsque vous l'affectez à a, mais il le fait supprime les nils lorsque vous l'affectez à b! Je suppose que cela a quelque chose à voir avec le flatMap surchargé et Swift choisir celui que nous ne voulions pas utiliser.

Vous pouvez temporairement résoudre le problème en le convertissant en type attendu:

a = a.flatMap { $0 } as [String]
a // == []

Mais cela peut être facile à oublier. Au lieu de cela, je recommanderais d'utiliser la méthode removeNils() ci-dessus.


Mise à jour

Il semble qu'il y ait une proposition de déprécier au moins une des (3) surcharges de flatMap: https://github.com/Apple/Swift-evolution/blob/master/proposals/0187- introduction-filtermap.md

61
Senseful

Swift 4

Si vous avez la chance d'utiliser Swift 4, vous pouvez filtrer les valeurs nulles en utilisant compactMap

array = array.compactMap { $0 }

Par exemple.

let array = [1, 2, nil, 4]
let nonNilArray = array.compactMap { $0 }

print(nonNilArray)
// [1, 2, 4]
11
Adam Wareing

Depuis Swift 2.0, il est possible d'ajouter une méthode qui fonctionne pour un sous-ensemble de types en utilisant les clauses where. Comme indiqué dans ce Apple Forum Thread cela peut être utilisé pour filtrer les valeurs nil d'un tableau. Les crédits vont à @nnnnnnnn et @SteveMcQwark.

Comme les clauses where ne prennent pas encore en charge les génériques (comme Optional<T>), une solution de contournement est nécessaire via un protocole.

protocol OptionalType {  
    typealias T  
    func intoOptional() -> T?  
}  

extension Optional : OptionalType {  
    func intoOptional() -> T? {  
        return self.flatMap {$0}  
    }  
}  

extension SequenceType where Generator.Element: OptionalType {  
    func flatten() -> [Generator.Element.T] {  
        return self.map { $0.intoOptional() }  
            .filter { $0 != nil }  
            .map { $0! }  
    }  
}  

let mixed: [AnyObject?] = [1, "", nil, 3, nil, 4]  
let nonnils = mixed.flatten()    // 1, "", 3, 4  
11
fabb

Swift 4

Cela fonctionne avec Swift 4:

protocol OptionalType {
    associatedtype Wrapped
    var optional: Wrapped? { get }
}

extension Optional: OptionalType {
    var optional: Wrapped? { return self }
}

extension Sequence where Iterator.Element: OptionalType {
    func removeNils() -> [Iterator.Element.Wrapped] {
        return self.flatMap { $0.optional }
    }
}

Tester:

class UtilitiesTests: XCTestCase {

    func testRemoveNils() {
        let optionalString: String? = nil
        let strings: [String?] = ["Foo", optionalString, "Bar", optionalString, "Baz"]
        XCTAssert(strings.count == 5)
        XCTAssert(strings.removeNils().count == 3)
        let integers: [Int?] = [2, nil, 4, nil, nil, 5]
        XCTAssert(integers.count == 6)
        XCTAssert(integers.removeNils().count == 3)
    }
}
5
Sajjon