在遍历一个 Array
的时候,我们可能会用 for .. in
map
flatMap
或者是 forEach
等等,这里谈一谈如何更好的用 flatMap
处理 [T?] 。
可能最先想到的方法就是配合 if let
。像这样:
var array: [Int?] = [1, 2, 3, 4, nil, 6]
for value in array { // 这里的 value 是 Int? 类型
if let value = value {
print(value) // 这里的 value 是 Int 类型
}
}
这没什么不好的,但我们的需求是遍历所有不为 nil
的值。从代码中直接去读的话,需要读两行。试试 where
?
for value in array where value != nil { // 这里的 value 是 Int? 类型
print(value!) // 这里的 value 仍然是 Int? 类型
}
当然也可能去用 filter
:
for value in array.filter({ $0 != nil }) {
print(value!) // 这里的 value 是 Int? 类型
}
这样的代码就比较尴尬了。目前来看用 where 并不能获得正确的类型推断。
事实上是因为我们这里的 where 操作并没有进行解包
试试 withoutNil()
?
for value in array.withoutNil() { // 这里的 value 是 Int 类型
print(value) // 这里的 value 是 Int 类型
}
当然还有 filterNil()
。
具体实现的代码参考如下:
public protocol OptionalType {
associatedtype Wrapped
var value: Wrapped? { get }
}
extension Optional: OptionalType {
/// Cast `Optional<Wrapped>` to `Wrapped?`
public var value: Wrapped? {
return self
}
}
extension SequenceType where Generator.Element: OptionalType {
@warn_unused_result
public func filterNil() -> [Generator.Element.Wrapped] {
return self.flatMap { $0.value }
}
@warn_unused_result
public func withoutNil() -> [Generator.Element.Wrapped] {
return self.flatMap { $0.value }
}
}
其实可以看到完全是一个实现效果,只是不同的方法名有不同的适用场景,刚刚的 for in
就适合 withoutNil()
。在链式转换我们的数据时,推荐 filterNil()
。
比如我们需要将 view
子视图中设置所有的 UIButton
的 backgroundColor
。
同样可以使用之前的几种方案 if let where filter
。
参考上面的 filterNil
:
for button in view.subviews.flatMap(type: UIButton.self) {
print(button) // yooo 直接就是 UIButton 了
}
实现过程也很简单:
extension SequenceType {
/// flatMap 出我们需要的类型
///
/// - Complexity: O(*M* + *N*), where *M* is the length of `self`
/// and *N* is the length of the result.
@warn_unused_result
public func flatMap<T>(type type: T.Type) -> [T] {
return flatMap { $0 as? T }
}
}
事实上 flatMap
还有更多更好玩的地方,我们完全可以用 flatMap
和 map
自由的变换出需要的数据。上面的两个情况都只是非常常见的场景,我们可以单独写一个方法。
再来看一个例子:
struct Phone {
var number: String?
var originalLabel: String?
init(number: String? = nil, originalLabel: String? = nil) {
self.number = number
self.originalLabel = originalLabel
}
}
struct User {
var name: String?
var phones: [Phone]?
init(name: String? = nil, phones: [Phone]? = nil) {
self.name = name
self.phones = phones
}
}
我们需要从 [User]
中拿出符合以下条件的结果:
name
不是 nil
,也就是有名字的 User
phone
的 number
不是 nil
,也就是要有手机号拿出以上结果后,最终返回一个 ["name": "", "number": ""]
。也就是说最终的 array
是根据 number
数量决定的。
不要问我为什么会有这样奇葩的需求,iPhone 上的通讯录就是这个样子。其实这部分是我从 Yep (opens new window) 中抽取出来的一个场景。PS:Yep 是个很好的学习项目,可以从中学到很多有意思的东西。
这部分笔者不打算再去解释了,留给读者做个思考:
typealias UploadContact = [String: String]
users
.flatMap { user -> (name: String, numbers: [String])? in
guard let name = user.name, phones = user.phones else { return nil }
return (name: name, numbers: phones.flatMap { $0.number })
}
.flatMap { contact -> [UploadContact] in
return contact.numbers.map { ["name": contact.name, "number": $0] }
}
以上完整代码见 Gist (opens new window) 。
先来看一下 filter
的声明:
extension SequenceType {
public func filter(@noescape includeElement: (Self.Generator.Element) throws -> Bool) rethrows -> [Self.Generator.Element]
}
通俗的说就是 [T] -> [T]
的过程,如果是 T?
,那就是 [T?] -> [T?]
。可以看到 filter
虽然是起到过滤筛选的作用,但事实上,这绝不是过滤 nil
的好方案,因为它并没有帮我们进行解包。那么 filter
的最佳使用场景是什么呢?我认为它和 where
有些相似,都是通过返回一个 Bool
去寻找我们需要的数据。类似这样:
[60, 70, 59].filter { $0 > 60 }
nil
选择 flatMap
filter