wake-up-neo.net

So verwenden Sie Swift flatMap, um Optionals aus einem Array herauszufiltern

Ich bin ein bisschen verwirrt um flatMap (zu Swift 1.2 hinzugefügt)

Angenommen, ich habe ein Array eines beliebigen optionalen Typs, z. 

let possibles:[Int?] = [nil, 1, 2, 3, nil, nil, 4, 5]

In Swift 1.1 würde ich einen Filter ausführen, gefolgt von einer Karte wie folgt:

let filtermap = possibles.filter({ return $0 != nil }).map({ return $0! })
// filtermap = [1, 2, 3, 4, 5]

Ich habe versucht, dies mit flatMap zu tun:

var flatmap1 = possibles.flatMap({
    return $0 == nil ? [] : [$0!]
})

und 

var flatmap2:[Int] = possibles.flatMap({
    if let exercise = $0 { return [exercise] }
    return []
})

Ich bevorzuge den letzten Ansatz (weil ich keinen erzwungenen Entpacken $0!... machen muss. Ich habe Angst vor diesen und vermeide sie um jeden Preis), außer dass ich den Array-Typ angeben muss.

Gibt es eine Alternative, die den Typ anhand des Kontextes herausfindet, aber nicht das erzwungene Auspacken hat?

21
MathewS

Seit Swift 4.1 können Sie compactMap verwenden:

let possibles:[Int?] = [nil, 1, 2, 3, nil, nil, 4, 5]
let actuals = possibles.compactMap { $0 }

(Swift 4.1 hat einige Überladungen von flatMap durch compactmap ..__ ersetzt. Wenn Sie mehr darüber erfahren möchten, lesen Sie zum Beispiel: https://useyourloaf.com/blog/replacing-flatmap-with-compactmap/ )

Mit Swift 2 b1 können Sie dies einfach tun

let possibles:[Int?] = [nil, 1, 2, 3, nil, nil, 4, 5]
let actuals = possibles.flatMap { $0 }

Bei früheren Versionen können Sie dies mit der folgenden Erweiterung vornehmen:

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

Ein Nachteil (was auch für Swift 2 gilt) besteht darin, dass Sie den Rückgabewert der Transformation möglicherweise explizit eingeben müssen:

let actuals = ["a", "1"].flatMap { str -> Int? in
    if let int = str.toInt() {
        return int
    } else {
        return nil
    }
}
assert(actuals == [1])

Weitere Informationen finden Sie unter http://airspeedvelocity.net/2015/07/23/changes-to-the-Swift-standard-library-in-2-0-betas-2-5/

35
Fizker

Ich mag immer noch die erste Lösung, die nur ein Es kann etwas kompakter geschrieben werden als

let filtermap = possibles.filter({ $0 != nil }).map({ $0! })

Aber flatMap() ohne Typanmerkung und ohne erzwungenes Auspacken ist möglich:

var flatmap3 = possibles.flatMap {
    flatMap($0, { [$0] }) ?? []
}

Das äußere flatMap ist die Array-Methode

func flatMap<U>(transform: @noescape (T) -> [U]) -> [U]

und das innere flatMap ist die Funktion

func flatMap<T, U>(x: T?, f: @noescape (T) -> U?) -> U?

Hier ist ein einfacher Leistungsvergleich (kompiliert im Freigabemodus) . Es zeigt, dass die erste Methode etwa um einen Faktor Von 10 schneller ist:

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
15
Martin R

Bezogen auf die Frage. Wenn Sie flatMap auf ein optionales Array anwenden, vergessen Sie nicht, Ihr Array optional zu erzwingen oder es zu erzwingen. Andernfalls wird flatMap für Optional und keine Objekte aufgerufen, die dem Sequence-Protokoll entsprechen. Ich habe diesen Fehler einmal gemacht, z. Wenn Sie leere Zeichenfolgen entfernen möchten:

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
0
Au Ris

Sie könnten reduce verwenden:

let flattened = possibles.reduce([Int]()) { 
        if let x = $1 { return $0 + [x] } else { return $0 } 
    }

Sie erklären immer noch den Typ, aber es ist etwas weniger aufdringlich.

0
letvargo

Da dies etwas ist, scheint ich am Ende ziemlich viel zu tun. Ich untersuche eine generische Funktion, um dies zu tun.

Ich habe versucht, eine Erweiterung zu Array hinzuzufügen, damit ich etwas wie possibles.unwraped tun konnte, aber ich konnte nicht herausfinden, wie eine Erweiterung für ein Array erstellt wird. Stattdessen benutzte man einen benutzerdefinierten Operator - der schwierigste Teil bestand darin herauszufinden, welchen Operator er wählen sollte. Am Ende wählte ich >!, um anzuzeigen, dass das Array > gefiltert wird und dann ! entpackt wird.

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]
0
MathewS