flatMap 和双重 Optional

上次刷微博的时候看到@唐巧大大发微博贴的几段代码,经过我整理成下面几种情况:

let arr: [Int?] = [1,3,nil,4]

let arr1: [Int?] = arr.flatMap { $0 } // [{some 1}, {some 3}, nil, {some 4}]

let arr2: [Int?] = arr.flatMap {
  next -> Int?? in
  next
} // [{some 1}, {some 3}, nil, {some 4}]

let arr3: [Int?] = arr.flatMap { 
    next -> Int? in
    next
} // [{some 1}, {some 3}, {some 4}]

let arr4 = arr.flatMap { $0 } // [1,3,4]

let arr5: [Int] = arr.flatMap { $0 } // [1,3,4]

我不理解为什么这几段代码会让 flatMap 有不同的行为,断断续续想了几天还没没有结果,于是决定去 StackOverFlow 上提这个问题,问题在这里,很幸运的是很快就有人回答了这个问题,并且回答得还不错,我把别人的答案和自己的理解记录下来。

相信很多人对 Array 的 flatMap command + click 进去看 flatMap 的声明的时候,都会发现 SequenceType 下面有两个,

public func flatMap<S : SequenceType>(transform: (Self.Generator.Element) throws -> S) rethrows -> [S.Generator.Element]

public func flatMap<T>(@noescape transform: (Self.Generator.Element) throws -> T?) rethrows -> [T]

第一个是用来把数组拍平的,第二个是用来把 nil 值去掉的,这里对这个问题有部分的解释,但还不够详细,于是我就去 Swift 的源代码中查找,终于找到了 flatMap 的实现,这里贴出部分的代码:

public func flatMap<SegmentOfResult : Sequence>(
    _ transform: (Elements.Iterator.Element) -> SegmentOfResult
  ) -> LazySequence<
    FlattenSequence<LazyMapSequence<Elements, SegmentOfResult>>> {
    return self.map(transform).flatten()
 }
  
public func flatMap<ElementOfResult>(
    _ transform: (Elements.Iterator.Element) -> ElementOfResult?
  ) -> LazyMapSequence<
    LazyFilterSequence<
      LazyMapSequence<Elements, ElementOfResult?>>,
    ElementOfResult
  > {
    return self.map(transform).filter { $0 != nil }.map { $0! }
}

第一个 flatMap,先对自己做 map 操作,然后对自己做 flatten 操作。

第二个 flatMap 比较有意思,先对自己做 map 操作,然后过滤掉 nil 值,最后将 optional 转化为一般值。

显然我们这里要讨论的是第二种 flatMap。

当我们不对 flatMap 的返回值做任何的限制的时候,对应上面的代码中的 arr4,此时 T 被推断为 Int,所以 tansform 闭包简化后的类型为 Int? -> Int?,注意这个闭包是被传给 map 用的。

看一下 Array 的 map 的声明:

public func map<T>(@noescape transform: (Self.Generator.Element) throws -> T) rethrows -> [T]

想想 map 的原理,很简单,其实就是遍历整个数组,对每个元素执行 transform 函数,然后返回一个包含这些元素的新数组。对于我们传入的 transform 闭包,这儿的 map 等于什么都没做,所以此时用于下一个 filter 操作的数组还是 [Int?],接下来就顺理成章了,剔除掉 nil 值,最后映射为非 optional 值。

对 arr5 来说,情况一样。

当我们指明 flatMap 的返回值为 [Int?] 时,情况就有所不同了。此时 T 被推断为 Int?,transform 闭包简化后的类型变为 Int? -> Int?? ,arr1 和 arr2 显然是等价的,我们先来分析一下,首先对遍历数组对数组的元素做 transform 操作,因为 transform 闭包返回的类型为 Int??,所以此时返回的新数组的元素的类型为 Int?? 或者更清楚一点为 Optional<Optional<Int>> ,接下来对数组剔除 nil 值,因为此时数组内并没有 nil(之前的 nil 被 map 为 Optional<nil>),所以 filter 并没起作用,最后 map 操作将元素强制解包为 Optional<Int>

到这里,事情还没结束,我最不能理解的是 arr3 这种情况,和 arr1 一样,返回值都被强制的指明为 [Int?],但是发生变化的是传入的 transform 闭包,本来应该要接收的类型为 Int? -> Int??,但是传入的却为 Int? -> Int? ,这为什么可以呢?

在我之前提到的 StackOverFlow 的回答中,这位热心的 @originaluser2 给我了这样的回答:

Because you explicitly annotate the return type of the closure to be Int?, the closure will get implicitly promoted from (Element) -> Int? to (Element) -> Int?? (closure return types can get freely promoted in the same way as other types) – rather than the element itself being promoted from Int? to Int??, as without the type annotation the closure would be inferred to be (Element) -> Int??

翻译下来就是说,Int? -> Int? 可以提升为 Int? -> Int??,或者说对于一个接受 Int? -> Int?? 的函数同样可以接受 Int? -> Int?

并且他举了下面这个例子,

func optionalIntArrayWithElement(closure: () -> Int??) -> [Int?] {
    let c = closure() // of type Int??
    if let c = c { // of type Int?
        return [c]
    } else {
        return []
    }
}

// another quirk: if you don't explicitly define the type of the optional (i.e write 'nil'),
// then nil won't get double wrapped in either circumstance
let elementA : () -> Int? = {Optional<Int>.None} // () -> Int?
let elementB : () -> Int?? = {Optional<Int>.None} // () -> Int??

// (1) nil gets picked up by the if let, as the closure gets implicitly upcast from () -> Int? to () -> Int??
let arr = optionalIntArrayWithElement(elementA)

// (2) nil doesn't get picked up by the if let as the element itself gets promoted to a double wrapped optional
let arr2 = optionalIntArrayWithElement(elementB)

if arr.isEmpty {
    print("nil was filtered out of arr") // this prints
}

if arr2.isEmpty {
    print("nil was filtered out of arr2") // this doesn't print
}

我们可以看到 optionalIntArrayWithElement 需要的是一个 () -> Int?? 的闭包,而 elementA 是一个 () -> Int? ,但是传进去完全没有问题。

接着看代码的一段注释:

another quirk: if you don't explicitly define the type of the optional (i.e write 'nil'), then nil won't get double wrapped in either circumstance

也就是说如果只单纯的把 nil 赋值给 Int?? 的变量,那么这个 nil 不会被双重 optionla 包裹,对于双重 optional 可以看看喵神这篇

这时候最难的部分来了,下面是我自己的理解,不知道是否完全正确,供大家讨论。

从一开始说起,首先我们声明 arr 为 [Int?],对于里面的 nil 值,此时和 optional<Int>.None 等价,当我们 map arr 时,也就是对里面的元素进行 transform 操作时,虽然此时的 transform 函数被提升为返回 Int?? ,但是由于元素的 nil 值为普通的 nil,所以此时 nil 并没有变化为 Optional<Optional<Int>>.None,而普通的非 nil 当然可以变化为 Optional<Optional<Int>>.Some(3)等。所以最后 nil 值被去掉了,但是其他的非 nil 值仍旧没变。

但是这里还有新的一个问题,就是当 transform 为默认返回为 Int?? 类型时,nil 值很显然被映射为了双重 nil,这是为啥呢?难道是默认的行为吗?期待有人能够分析解答。

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

推荐阅读更多精彩内容