flatMap 温顾知新 —— 参照 Swift 源码实现讲解

0. 前言

非常感谢喵神对本文的指正,并且引入“降维”一说。对于 Optional<T> 调用 flatMap 方法,源码实现内部首先进行解包行为后传值到闭包中(见图),这里可视为“降维”,当然我觉得应该侧重“map”多一些;而对于sequence来说,调用flatMap是否存在“降维”取决于具体处理,至于过滤nil,那不过是 flatMap的内部实现罢了。就像下文中我说到的,侧重点在于flatmap中的“map”过程,由什么到什么的转换,拿Sequence为例,前面的什么已经由数组元素决定了,后者由你决定,你可以把一个Int类型变成[Int],也可以把[Int] 变成Int,完全看你心情。

1.png
2.png
3.png
4.png
5.png

1. 为什么写这篇文章

学习 swift talk #01 Networking,我发现 Chris Eidhof 在代码中频繁使用 flatMap 来处理回调的数据,就像 data.flatMap —— data 是可选类型。网上有关于 flatMap 各式各样的教程,告诉如何调用,调用结果会是什么,一般总结是:flatMap可以应用于元素为nil的数组,最后处理得到的返回数组也将是剔除 nil 的结果。

至于写这篇文章的目的:

  1. 大部分教程已经离我们太“久远”
  2. 大部分教程并没有全面讲解 flatMap 的使用,基本都是围绕数组展开,而本文则提供了众多场景来告知为何应该这么用以及举一反三;
  3. 面试时经常问 mapflatMap 的区别或 flatMap 的作用,其实就是两个字“降维”(引自喵神的swift 100 tips);
  4. 周末写篇博客是我的计划之一

2. Optional<T> 可选类型调用 flatMap 方法

当你对一个可选类型调用 flatMap 的时候,你可以看到 Swift 实际提供了如下例程供我们参考:

let optionalInt : Int? = Optional<Int>(3)

let result1 = optionalInt.flatMap { (wrapped) -> String? in
    return "\(wrapped)"
}
print(result1) /// Optional("3")

注意:flatMap 应用在可选类型上时,返回类型同样是可选类型 Optional<T>, 而 wrapped 是什么呢,它是 optionalInt 解包后的值,等同于 optionalInt! —— 当然前提是 optionalInt 不为 nil

这么讲解,可能无法让你理解,甚至混淆你之前的概念,所以我下面会给出 swift 源码实现,加深大家对 flatMap 的理解,我个人倾向于在学习某个知识点时,尽量深入一些,不要停留在表面的使用 —— 当然越深入所需要花费的精力和时间就越多。

ok,继续可选类型下flatMap swift 源码的实现:

public func flatMap<U>(_ transform: (Wrapped) throws -> U? ) rethrows -> U? {
 switch self {
  case .some(let y):
    return try transform(y)
  case .none:
    return .none
  }
 }

可选类型的概念:要么值不存在 .none(nil),要么有值 case .some(let y),这里 y 就是解包后的值,然后传入 tranform 闭包中,当然闭包处理结果也是有可能返回 nil 的,这取决于你的处理方式了,这也是为什么 tranform 闭包类型为 (Wrapped) throws -> U?,同时 flatMap 返回值类型也是可选类型U?

知识点:对于可选类型来说,Wrapped 一个值是一个“升维”操作,而对可选类型进行 UnWrapped 操作,是一个“降维”操作,请参照源码。

3. Sequence 调用 flatMap 方法

这里存在几种情况:

  1. [Int] 类型(即Array<T>) -> [1,2,3,4]。数组中的元素均不为 nil
  2. [Int?] 类型(即Array<T?>) -> [1,2,nil,4]。数组中的元素允许存在 nil
  3. 如果混合可选类型的话,还会衍生出 [Int]?[Int?]? 两种情况;

这里我们会先分析 1 和 2,是时候先来看看 Sequence 中 flatMap 的实现:

public func flatMap<ElementOfResult>(
 _ transform: @escaping (Elements.Element) -> ElementOfResult?) -> LazyMapSequence<LazyFilterSequence<LazyMapSequence<Elements, ElementOfResult?>>, ElementOfResult > {
  return self.map(transform).filter { $0 != nil }.map { $0! }
 }

定义看起来有点让人“瘆得慌”,尤其是返回类型!不过这里我建议你只需要关注两点:

  1. transform 的类型 (Elements.Element) -> ElementOfResult?,传入参数类型 Elements.Element 由数组中的元素类型决定,比如 [Int] 数组中 Element 就是 Int;而 [Int?] 数组中 Element 就是可选类型 Int?,至于 ElementOfResult 泛型,这取决你。
  2. 内部实现,其实就是间接调用了 map 方法,我个人很喜欢链式调用,实在是太酷了,就像 self.map(transform).filter { $0 != nil }.map { $0! },理解起来也很简单,往 map 函数中传入闭包 tranform 对每个元素做处理,然后结果值调用 filter 剔除值为 nil 的元素,最后调用 map 依次对结果中的元素做解包处理——要知道此时数组中可不存在 nil 值了,请大胆放心的解包吧。

至此,部分同学应该会对上述两点产生一些不解或疑惑,可能会问一些问题:

Q1: 调用 self.map(transform),那么 transform 传入参数 Elements.Element 是什么类型?
A1: 再次强调,Elements.Element是由数组元素类型决定。

Q2:怎么理解“至于 ElementOfResult 泛型,这取决你。”
A2:Elements.Element 类型是由数组类型决定的,而我们希望数组中每个元素应用 transform 闭包后的结果值类型是由我们决定的,比如我们希望是字符串类型,那么实际代码调用的时候用 String 替换 ElementOfResult,接着 transform 中的 ElementOfResult 又决定了 func flatMap<ElementOfResult>() 中的 ElementOfResult —— 也就是 String 类型。

3.1 [Int] 类型

例程:

let noneOptionalArray = [1,2,3,4]
let result2 = noneOptionalArray.flatMap { (x) -> Int? in
    guard x > 2 else {
        return nil // 小于 2 的情况认为是不符合预期 返回 nil,其他情况进行加一操作,因此返回值类型为 `Int?`
    }
    return x+1
}
print(result2)//[ 4, 5]

前面说到 transform 闭包类型中的 Elements.Element 是由数组元素类型决定,所以这里 x 的类型为 Int,此外我们的 transform 希望对每个元素做 +1 处理 —— 那么还是个 Int 类型,所以我们将 ElementOfResult 替换成 Int,当然如果你想数组元素格式化成字符串,那么这里返回值类型就是 String?;参考源码我们知道数组应用了 transform 之后会调用 filter 剔除值为 nil 的元素,剩下的都是 Optional 中的 .some,所以最后一步就是解包 map{ $0!}

学习过程中,我更改了闭包中实现——不再设定大于2就返回nil的处理,显然这没有任何问题:

let noneOptionalArray = [1,2,3,4]
let result2 = noneOptionalArray.flatMap { (x) -> Int? in
    return x+1
}
print(result2)//[2, 3, 4, 5]

接着我又在想,既然闭包不可能返回 nil,那返回 Int? 可选类型干嘛,应该返回 Int 也是Ok的吧,于是我又修改了代码:

let noneOptionalArray = [1,2,3,4]
let result3 = noneOptionalArray.flatMap { (x) -> Int in
    return x+1
}
print(result3)//[2, 3, 4, 5]

这也是Ok的,但是倘若你在闭包处理中加会那段大于2返回nil的限制代码,Xcode会立即提示你的返回值类型错误,这也是我上面说到的,ElementOfResult 的类型由你决定。

3.2 [Int?] 类型

讲完 [Int] 类型,本节实际上就没有任何难度了,这里给出几个例程

let optionalArray = [1,nil,2,3]
// 由于optionalArray里面的类型是 Optional<Int> 所以这里的x也是可选类型
let result4 = optionalArray.flatMap { (x) -> Int? in
    guard let xx = x else { return nil }
    return xx + 1
}
print(result4)//[2, 3, 4]

optionalArray 的类型为 Array<Int?>,因此 x 的类型为 Int?,闭包接收到的元素分别为.some(1),.none,.some(2).some(3)可选类型,这也是为什么闭包处理中首先对 x 进行绑定解包,如果 x 为 nil,直接返回 nil,否则进行+1操作。

当然3.1小节中的好奇我同样带到了这里,我一定要返回 Int?吗? 对于不喜欢的 nil 我希望返回 0 就ok拉。

let optionalArray = [1,nil,2,3]
// 由于optionalArray里面的类型是 Optional<Int> 所以这里的x也是可选类型
let result5 = optionalArray.flatMap { (x) -> Int in
    guard let xx = x else { return 0 }
    return xx + 1
}
print(result5)//[2, 0, 3, 4]

由此可以看到 tranform 传入 x 的类型我们无法左右,但是!!闭包返回值类型我们却可以随心所欲的改变,这一切取决于你。

4. 可选类型混合Sequence

有了上面的铺垫,下面相对来说会顺风顺水一些,先来看 [Int]? 类型:

let optionalWrappedArray = Optional<Array<Int>>([1,2,3])

let result5 = optionalWrappedArray.flatMap { (array) -> Array<String>? in // 1
    return array.map({ (element) -> String in // 2
        return "element:\(element)"
    })
}
print(result5) // Optional(["element:1", "element:2", "element:3"])

首先 optionalWrappedArray 整体来看是一个可选类型,要么没有值 nil,要么有值是一个数组,而调用 flatMap 后返回值类型同样是一个可选类型;注意 1 中的 array,有了源码的讲解,我们知道这里 array 是解包后的值,也就是 [1,2,3],接着我们调用 map 方法将元素格式化成字符串输出。

如果你跟随我的节奏码代码,你应该注意到输入 optionalWrappedArray. 智能提示会有很多方法,你可以会不小心选择到 flatMap(<#T##transform: (Int) throws -> ElementOfResult?##(Int) throws -> ElementOfResult?#>) 方法,然后调用方式是这样了:

let result6 = optionalWrappedArray?.flatMap({ (x) -> String? in
    return "element:\(x + 1)"
})
print(result6)// Optional(["element:2", "element:3", "element:4"]) 

这里你需要仔细观察两者的不同,相信你的火眼金睛 —— 这么大的一个问好 ? 应该已经注意到了吧!对于 optionalWrappedArray? 已经产生了一个解包行为,实际调用等价于 [1,2,3].flatMap (而对于 nil.flatMap 返回自然是 nil 喽)这又回到了小节 3.1 的内容,不妨自己回顾下,理下思路。

最后我们说说 [Int?]? 类型来结束本文:

let doubleOptional = Optional<Array<Int?>>([1,nil,2,3])
let result7 = doubleOptional.flatMap { (array:[Int?]) -> String? in
    return array.reduce("", { (res:String, number:Int?) -> String in
        guard let num = number else { return res + " number:null" }
        return res + " number:\(num)"
    })
}
print(result7)// Optional(" number:1 number:null number:2 number:3")

以及提前解包然后调用 flatMap

let result8 = doubleOptional?.flatMap({ (x) -> String? in
    guard let element = x else { return nil}
    return "element \(element * element)"
})
print(result8)//Optional(["element 1", "element 4", "element 9"])

最后我希望大家思考下为什么最后四个返回值类型都是可选类型?我们日常开发希望得到的是可选类型中的值,那么又该如何做呢?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容