Comment utiliser swift flatMap pour filtrer les options d'un tableau
Je suis un peu confus autour de flatMap (ajouté à Swift 1.2)
Disons que j'ai un tableau d'un type optionnel par exemple
let possibles:[Int?] = [nil, 1, 2, 3, nil, nil, 4, 5]
Dans Swift 1.1, je ferais un filtre suivi d'une carte comme celle-ci:
let filtermap = possibles.filter({ return $0 != nil }).map({ return $0! })
// filtermap = [1, 2, 3, 4, 5]
J'ai essayé de le faire en utilisant flatMap de quelques façons:
var flatmap1 = possibles.flatMap({
return $0 == nil ? [] : [$0!]
})
Et
var flatmap2:[Int] = possibles.flatMap({
if let exercise = $0 { return [exercise] }
return []
})
Je préfère la dernière approche (parce que je n'ai pas à faire un déballage forcé $0!
... Je suis terrifié pour ceux-ci et les éviter à tout prix) sauf que je dois spécifier le tableau type.
Existe-t-il une alternative qui détermine le type par contexte, mais n'a pas le déballage forcé?
5 réponses
Avec Swift 2 b1, vous pouvez simplement faire
let possibles:[Int?] = [nil, 1, 2, 3, nil, nil, 4, 5]
let actuals = possibles.flatMap { $0 }
Pour les versions antérieures, vous pouvez corriger ceci avec l'extension suivante:
extension Array {
func flatMap<U>(transform: Element -> U?) -> [U] {
var result = [U]()
result.reserveCapacity(self.count)
for item in map(transform) {
if let item = item {
result.append(item)
}
}
return result
}
}
Une mise en garde (ce qui est également vrai pour Swift 2) est que vous devrez peut-être taper explicitement la valeur de retour de la transformation:
let actuals = ["a", "1"].flatMap { str -> Int? in
if let int = str.toInt() {
return int
} else {
return nil
}
}
assert(actuals == [1])
Pour plus d'informations, voir http://airspeedvelocity.net/2015/07/23/changes-to-the-swift-standard-library-in-2-0-betas-2-5/
J'aime toujours la première solution, qui ne crée qu'un intermédiaire tableau. Il peut être écrit un peu plus compact comme
let filtermap = possibles.filter({ $0 != nil }).map({ $0! })
, Mais flatMap()
, sans annotation de type et sans forcé
le déballage est possible:
var flatmap3 = possibles.flatMap {
flatMap($0, { [$0] }) ?? []
}
L'extérieur flatMap
est la méthode de Tableau
func flatMap<U>(transform: @noescape (T) -> [U]) -> [U]
Et l'intérieur flatMap
est la fonction
func flatMap<T, U>(x: T?, f: @noescape (T) -> U?) -> U?
Voici une simple comparaison des performances (compilée en mode Release). Il montre que la première méthode est plus rapide, environ une facteur sur 10:
let count = 1000000
let possibles : [Int?] = map(0 ..< count) { $0 % 2 == 0 ? $0 : nil }
let s1 = NSDate()
let result1 = possibles.filter({ $0 != nil }).map({ $0! })
let e1 = NSDate()
println(e1.timeIntervalSinceDate(s1))
// 0.0169369578361511
let s2 = NSDate()
var result2 = possibles.flatMap {
flatMap($0, { [$0] }) ?? []
}
let e2 = NSDate()
println(e2.timeIntervalSinceDate(s2))
// 0.117663979530334
Vous pouvez utiliser reduce
:
let flattened = possibles.reduce([Int]()) {
if let x = $1 { return $0 + [x] } else { return $0 }
}
Vous déclarez toujours le type, mais c'est un peu moins intrusif.
Comme c'est quelque chose que je semble finir par faire beaucoup, j'explore une fonction générique pour le faire.
J'ai essayé d'ajouter une extension au tableau pour pouvoir faire quelque chose comme possibles.unwraped
mais je n'ai pas pu comprendre comment faire une extension sur un tableau. Au lieu personnalisé opérateur -- la partie la plus difficile ici est d'essayer de comprendre quel opérateur choisir. À la fin, j'ai choisi >!
pour montrer que le tableau est filtré >
puis déballé !
.
let possibles:[Int?] = [nil, 1, 2, 3, nil, nil, 4, 5]
postfix operator >! {}
postfix func >! <T>(array: Array<T?>) -> Array<T> {
return array.filter({ $0 != nil }).map({ $0! })
}
possibles>!
// [1, 2, 3, 4, 5]
Lié à la question. Si vous appliquez flatMap
à un tableau optionnel, n'oubliez pas de déballer ou de forcer le déballage de votre tableau sinon il appellera flatMap
sur Optional
et non des objets conformes au Protocole Sequence
. J'ai fait cette erreur Une fois, par exemple lorsque vous voulez supprimer des chaînes vides:
var texts: [String]? = ["one", "two", "", "three"] // has unwanted empty string
let notFlatMapped = texts.flatMap({ $0.count > 0 ? $0 : nil })
// ["one", "two", "", "three"], not what we want - calls flatMap on Optional
let flatMapped = texts?.flatMap({ $0.count > 0 ? $0 : nil })
// ["one", "two", "three"], that's what we want, calls flatMap on Array