23

我对 flatMap 有点困惑(添加到 Swift 1.2)

假设我有一些可选类型的数组,例如

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

在 Swift 1.1 中,我会做一个过滤器,然后是这样的地图:

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

我一直在尝试使用 flatMap 几种方式来做到这一点:

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

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

我更喜欢最后一种方法(因为我不必强制展开$0!......我对这些感到害怕并不惜一切代价避免它们)除了我需要指定 Array 类型。

是否有另一种方法可以根据上下文确定类型,但没有强制展开?

4

5 回答 5

39

Swift 4.1开始,您可以使用 compactMap:

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

(Swift 4.1 用 compactmap 替换了一些 flatMap 的重载。如果您对此感兴趣,请参阅例如: https ://useyourloaf.com/blog/replacing-flatmap-with-compactmap/ )

使用Swift 2 b1,你可以简单地做

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

对于早期版本,您可以使用以下扩展名对此进行填充:

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/

于 2015-07-24T17:14:55.293 回答
15

我仍然喜欢第一个解决方案,它只创建一个中间数组。它可以稍微紧凑地写成

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

但是flatMap()没有类型注释并且没有强制展开是可能的:

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

外部flatMap是数组方法

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

内部flatMap是函数

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

这是一个简单的性能比较(以Release模式编译)。它表明第一种方法更快,大约是 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
于 2015-04-25T20:53:53.260 回答
2

Related to the question. If you are applying flatMap to an optional array, do not forget to optionally or force unwrap your array otherwise it will call flatMap on Optional and not objects conforming to Sequence protocol. I made that mistake once, E.g. when you want to remove empty strings:

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
于 2018-06-27T06:58:09.027 回答
0

你可以使用reduce

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

您仍然在声明类型,但它稍微不那么突兀。

于 2015-04-25T20:43:42.200 回答
0

由于这是我似乎最终做了很多的事情,我正在探索一个通用函数来做到这一点。

我试图向 Array 添加一个扩展,这样我就可以做类似的事情,possibles.unwraped但不知道如何在 Array 上进行扩展。取而代之的是使用自定义运算符——这里最困难的部分是试图找出选择哪个运算符。最后,我选择>!显示数组正在被过滤>,然后被解包!

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]
于 2015-04-26T23:05:17.203 回答