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!)
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]
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]
Il existe 2 flatMap
fonctions:
Un flatMap
est utilisé pour supprimer les valeurs non nulles, comme illustré ci-dessus. Voir - https://developer.Apple.com/documentation/Swift/sequence/2907182-flatmap
L'autre flatMap
est utilisé pour concaténer les résultats. Voir - https://developer.Apple.com/documentation/Swift/sequence/2905332-flatmap
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 .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.Pour récapituler, il existe deux versions de la fonction en question, toutes deux, malheureusement, nommées flatMap
.
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]
}
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.
// 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 .)
let a: [Int?] = [1, nil, 3]
a.removeNils() == [1, 3]
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:
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
.T
sur OptionalType
, il utilise le même nom que Optional
(Wrapped
).map{}.filter{}.map{}
, ce qui conduit à O(M + N)
fois, je fais une boucle dans le tableau une fois.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.
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.
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
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]
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
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 }
}
}
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)
}
}