arrays - type - Crear una extensión para filtrar nils desde una matriz en Swift
generic swift (6)
Swift 4
Esto funciona con 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 }
}
}
Prueba:
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)
}
}
Intento escribir una extensión a Array que permita transformar una matriz de T''s opcionales en una matriz de T no opcionales.
por ejemplo, esto podría escribirse como una función gratuita como esta:
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
}
Pero no puedo hacer que esto funcione como una extensión. Intento decirle al compilador que la extensión solo se aplica a las matrices de valores opcionales. Esto es lo que tengo hasta ahora:
extension Array {
func filterNils<U, T: Optional<U>>() -> [U] {
return filter({ $0 != nil }).map({ $0! })
}
}
(no compila!)
Swift 4
Si tiene la suerte de utilizar Swift 4, puede filtrar los valores nulos utilizando compactMap
array = array.compactMap { $0 }
P.ej
let array = [1, 2, nil, 4]
let nonNilArray = array.compactMap { $0 }
print(nonNilArray)
// [1, 2, 4]
A partir de Swift 2.0, no necesita escribir su propia extensión para filtrar valores nulos de una matriz, puede usar flatMap
, que aplana la matriz y filtra nils:
let optionals : [String?] = ["a", "b", nil, "d"]
let nonOptionals = optionals.flatMap{$0}
print(nonOptionals)
Huellas dactilares:
[a, b, d]
Nota:
Hay 2 funciones de flatMap
:
Un
flatMap
se usa para eliminar valores no nulos que se muestran arriba. Consulte: https://developer.apple.com/documentation/swift/sequence/2907182-flatmapEl otro
flatMap
se usa para concatenar resultados. Consulte: https://developer.apple.com/documentation/swift/sequence/2905332-flatmap
Desde Swift 2.0, es posible agregar un método que funcione para un subconjunto de tipos mediante el uso de cláusulas where
. Como se discutió en este tema del answer esto se puede usar para filtrar valores nil
de una matriz. Los créditos van a @nnnnnnnn y @SteveMcQwark.
Como las cláusulas where
aún no admiten genéricos (como la Optional<T>
), se necesita una solución mediante un protocolo.
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
No es posible restringir el tipo definido para una estructura o clase genérica: la matriz está diseñada para funcionar con cualquier tipo, por lo que no puede agregar un método que funcione para un subconjunto de tipos. Las restricciones de tipo solo se pueden especificar al declarar el tipo genérico
La única forma de lograr lo que necesita es crear una función global o un método estático, en este último caso:
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)
O simplemente use flatMap
, que puede usarse para eliminar todos los valores nulos:
[1, 2, nil, 4].flatMap { $0 } // Returns [1, 2, 4]
Swift 4 actualización
Como señaló @Dinesh, en Swift 4, flatMap
está en desuso, por lo que debe usar compactMap
en compactMap
lugar
TL; DR
Para evitar posibles errores / confusión, no use array.flatMap { $0 }
para eliminar nils; utilice un método de extensión como array.removeNils()
lugar (implementación a continuación, actualizada para Swift 3.0 ).
Aunque array.flatMap { $0 }
funciona la mayor parte del tiempo, hay varias razones para favorecer una extensión array.removeNils()
:
-
removeNils
describe exactamente lo que quiere hacer : eliminar valoresnil
. Alguien que no esté familiarizado conflatMap
debería buscarlo y, cuando lo busquen, si prestan mucha atención, llegarán a la misma conclusión que mi próximo punto; -
flatMap
tiene dos implementaciones diferentes que hacen dos cosas completamente diferentes . Basado en la verificación de tipos, el compilador decidirá cuál se invoca. Esto puede ser muy problemático en Swift, ya que la inferencia de tipo se usa mucho. (Por ejemplo, para determinar el tipo real de una variable, es posible que deba inspeccionar varios archivos). Un refactor podría hacer que su aplicación invoque la versión incorrecta deflatMap
que podría generar errores difíciles de encontrar . - Dado que hay dos funciones completamente diferentes, hace que entender
flatMap
mucho más difícil ya que puede combinar fácilmente los dos . -
flatMap
puede llamar aflatMap
en arreglos no opcionales (ej.[Int]
), así que si refactoriza una matriz de[Int?]
a[Int]
puede dejar accidentalmenteflatMap { $0 }
cuales el compilador no lo advertirá . En el mejor de los casos, simplemente se devolverá a sí mismo, en el peor de los casos hará que se ejecute la otra implementación, lo que podría conducir a errores. - En Swift 3, si no echas explícitamente el tipo de devolución, el compilador elegirá la versión incorrecta , lo que ocasiona consecuencias imprevistas. (Consulte la sección Swift 3 a continuación)
- Finalmente, ralentiza el compilador porque el sistema de verificación de tipos necesita determinar cuál de las funciones sobrecargadas llamar.
Para recapitular, hay dos versiones de la función en cuestión, ambas desafortunadamente, llamadas flatMap
.
Aplanar secuencias eliminando un nivel de anidamiento (por ejemplo
[[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] }
Eliminar elementos de una secuencia (por ejemplo
[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 es el que las personas usan para eliminar nils pasando { $0 }
como transform
. Esto funciona porque el método realiza un mapa, luego filtra todos los elementos nil
.
Usted se estará preguntando "¿Por qué Apple no renombró el n. ° 2 para removeNils()
"? Una cosa a tener en cuenta es que usar flatMap
para eliminar nils no es el único uso del n. ° 2. De hecho, dado que ambas versiones tienen una función de transform
, pueden ser mucho más poderosas que los ejemplos anteriores.
Por ejemplo, # 1 podría dividir fácilmente una matriz de cadenas en caracteres individuales (aplanar) y poner mayúsculas en cada letra (mapa):
["abc", "d"].flatMap { $0.uppercaseString.characters } == ["A", "B", "C", "D"]
Mientras que el número 2 podría eliminar fácilmente todos los números pares (aplanar) y multiplicar cada número por -1
(mapa):
[1, 2, 3, 4, 5, 6].flatMap { ($0 % 2 == 0) ? nil : -$0 } == [-1, -3, -5]
(Tenga en cuenta que este último ejemplo puede hacer que Xcode 7.3 gire durante mucho tiempo porque no hay tipos explícitos establecidos. Prueba adicional de por qué los métodos deberían tener diferentes nombres).
El verdadero peligro de usar ciegamente flatMap { $0 }
para eliminar nil
s no viene cuando lo llamas [1, 2]
, sino cuando lo llamas en algo como [[1], [2]]
. En el primer caso, invocará la invocación n. ° 2 inofensivamente y devolverá [1, 2]
. En este último caso, puede pensar que haría lo mismo (devuelva inofensivamente [[1], [2]]
ya que no hay valores nil
), pero en realidad devolverá [1, 2]
ya que está utilizando la invocación n. ° 1.
El hecho de que flatMap { $0 }
se use para eliminar nil
parece ser más community recommendation community Swift que una proveniente de Apple. Quizás si Apple nota esta tendencia, eventualmente proporcionarán una función removeNils()
o algo similar.
Hasta entonces, nos quedará con nuestra propia solución.
Solución
// 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
}
}
(Nota: No te confundas con element.map
... no tiene nada que ver con el flatMap
discutido en esta publicación. Está usando la función de map
Optional
para obtener un tipo opcional que puede ser desenvuelto. Si omites esta parte , obtendrá este error de sintaxis: "error: el inicializador para el enlace condicional debe tener el tipo opcional, no el ''elemento generador de sí mismo''." Para obtener más información sobre cómo nos ayuda el map()
, consulte esta respuesta que escribí sobre cómo agregar un método de extensión en SequenceType para contar no-nils .)
Uso
let a: [Int?] = [1, nil, 3]
a.removeNils() == [1, 3]
Ejemplo
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''
(Observe cómo en el último, flatMap devuelve [1, 2, 3, 4]
mientras que se esperaba que removeNils () devolviera [[1], [2, 3], [4]]
).
La solución es similar a la answer @fabb vinculada a.
Sin embargo, hice algunas modificaciones:
- No nombré el método
flatten
, ya que ya hay un métodoflatten
para los tipos de secuencia, y dar el mismo nombre a métodos completamente diferentes es lo que nos metió en este lío en primer lugar. Sin mencionar que es mucho más fácil malinterpretar lo queremoveNils
queremoveNils
. - En lugar de crear un nuevo tipo
T
enOptionalType
, usa el mismo nombre que usaOptional
(Wrapped
). - En lugar de ejecutar el
map{}.filter{}.map{}
, que lleva aO(M + N)
tiempo, recorro el conjunto una vez. - En lugar de usar
flatMap
para ir deGenerator.Element
aGenerator.Element.Wrapped?
, Uso elmap
No es necesario devolver valoresnil
dentro de la función demap
, por lo que elmap
será suficiente. Al evitar la funciónflatMap
, es más difícil combinar otro método (es decir, tercero) con el mismo nombre que tiene una función completamente diferente.
El único inconveniente de utilizar removeNils
vs. flatMap
es que el verificador de tipos puede necesitar un poco más de alusión:
[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()
No he investigado mucho, pero parece que puedes agregar:
extension SequenceType {
func removeNils() -> Self {
return self
}
}
si desea poder invocar el método en matrices que contienen elementos no opcionales. Esto podría hacer un cambio de nombre masivo (por ejemplo, flatMap { $0 }
-> removeNils()
) más fácil.
¡Asignar a uno mismo es diferente a asignarle una nueva variable!
Eche un vistazo al siguiente código:
var a: [String?] = [nil, nil]
var b = a.flatMap{$0}
b // == []
a = a.flatMap{$0}
a // == [nil, nil]
Sorprendentemente, a = a.flatMap { $0 }
no elimina nils cuando se lo asigna a a
, pero elimina nils cuando se lo asigna a b
! Mi suposición es que esto tiene algo que ver con el flatMap
y Swift eligiendo el que no queríamos usar.
Puede resolver temporalmente el problema al convertirlo al tipo esperado:
a = a.flatMap { $0 } as [String]
a // == []
Pero esto puede ser fácil de olvidar. En cambio, recomendaría usar el método removeNils()
anterior.
Actualizar
Parece que hay una propuesta para depreciar al menos una de las (3) sobrecargas de flatMap
: https://github.com/apple/swift-evolution/blob/master/proposals/0187-introduce-filtermap.md