Как использовать swift flatMap для фильтрации optionals из массива

Я немного запутался вокруг flatMap (добавлен в Swift 1.2)

скажем, у меня есть массив некоторого необязательного типа, например

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

в Swift 1.1 я бы сделал фильтр, а затем такую карту:

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

Я пытался сделать это с помощью flatMap несколькими способами:

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

и

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

Я предпочитаю последний подход (потому что мне не нужно делать принудительное разворачивание !... Я боюсь за них и избегаю их любой ценой.) за исключением того, что мне нужно указать тип массива.

есть ли альтернатива, которая определяет тип по контексту, но не имеет принудительного разворачивания?

5 ответов


С Swift 2 b1 вы можете просто сделать

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

для более ранних версий, вы можете оболочек с следующим расширением:

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

одно предостережение (которое также верно для Swift 2) заключается в том, что вам может потребоваться явно ввести возвращаемое значение преобразования:

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

подробнее http://airspeedvelocity.net/2015/07/23/changes-to-the-swift-standard-library-in-2-0-betas-2-5/


мне все еще нравится первое решение, которое создает только одно промежуточное матрица. Его можно немного более компактно записать как

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

но flatMap() без аннотации типа и без принудительной возможна распаковка:

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

внешний flatMap метод выбора

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

и внутри flatMap функция

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

вот простое сравнение производительности (скомпилировано в режиме выпуска). Это показывает, что первый метод быстрее, примерно в два раза из 10:

let count = 1000000
let possibles : [Int?] = map(0 ..< count) {  % 2 == 0 ?  : nil }

let s1 = NSDate()
let result1 = possibles.filter({  != nil }).map({ ! })
let e1 = NSDate()
println(e1.timeIntervalSinceDate(s1))
// 0.0169369578361511

let s2 = NSDate()
var result2 = possibles.flatMap {
    flatMap(, { [] }) ?? []
}
let e2 = NSDate()
println(e2.timeIntervalSinceDate(s2))
// 0.117663979530334

можно использовать reduce:

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

вы все еще объявляете тип, но он немного менее навязчивый.


поскольку это то, что я, кажется, в конечном итоге делаю довольно много, я изучаю общую функцию для этого.

Я попытался добавить расширение в массив, чтобы я мог сделать что-то вроде possibles.unwraped но не мог понять, как сделать расширение в массиве. Вместо этого используется пользовательский оператор - самая сложная часть здесь пыталась выяснить, какой оператор выбрать. В конце концов я выбрал >! чтобы показать, что массив фильтруется > а потом развернул !.

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

postfix operator >! {}

postfix func >! <T>(array: Array<T?>) -> Array<T> {
    return array.filter({  != nil }).map({ ! })
}

possibles>!
// [1, 2, 3, 4, 5]

связанный с вопросом. Если вы применяете flatMap для необязательного массива не забудьте дополнительно или принудительно развернуть массив, иначе он вызовет flatMap on Optional и не объекты, соответствующие Sequence протокол. Однажды я совершил эту ошибку, например. если вы хотите удалить пустые строки:

var texts: [String]? = ["one", "two", "", "three"] // has unwanted empty string

let notFlatMapped = texts.flatMap({ .count > 0 ?  : nil })
// ["one", "two", "", "three"], not what we want - calls flatMap on Optional

let flatMapped = texts?.flatMap({ .count > 0 ?  : nil })
// ["one", "two", "three"], that's what we want, calls flatMap on Array