用更 Swifty 的代码遍历数据

# 遍历 [T?]

在遍历一个 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 子视图中设置所有的 UIButtonbackgroundColor

同样可以使用之前的几种方案 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 还有更多更好玩的地方,我们完全可以用 flatMapmap 自由的变换出需要的数据。上面的两个情况都只是非常常见的场景,我们可以单独写一个方法。

再来看一个例子:

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
  • phonenumber 不是 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