Swift 中的多尾随闭包(Multiple Trailing Closures)

[ 本文运行环境:Xcode12_beta_6 (Swift 5.3) ]

多尾随闭包(Multiple Trailing Closures)

尾随闭包在开发中随处可见:

    // 定义:
    open class func animate(withDuration duration: TimeInterval, animations: @escaping () -> Void)
    
    // 调用
    UIView.animate(withDuration: 0.3) {
        // 各种动画
    }

在 Swift5.3 之前,当有多个尾随闭包时写法是这样的:

    // 定义
    open class func animate(withDuration duration: TimeInterval, animations: @escaping () -> Void, completion: ((Bool) -> Void)? = nil)
    
    // 调用
    UIView.animate(withDuration: 0.3, animations: {
        // 各种动画
    }) { (finish) in
        // ???
    }

可以发现,这里的多尾随闭包有个缺点:调用时最后一个闭包没有名字。这样造成的后果是代码可读性差,对于不熟悉调用方法的开发者来说,就得去看方法的定义,运气差点接手了个毫无规范的前辈留下的自定义方法,没写注释,参数名再整个abc,那还得去看方法实现。

苹果很早就注意到了这个问题,但却只是在规范中建议在调用含有多个闭包参数的方法中避免使用尾随闭包。意思是建议使用其他方式实现多尾随闭包的结构。

这很不 "Swift"。

好在 Swift5.3 中对尾随闭包进行了优化,调用多尾随闭包时最后一个闭包将会显示出参数名:

    UIView.animate(withDuration: 0.3) {
        // 各种动画
    } completion: { (finish) in
        // completion - 动画结束
    }

老夫这强迫症终于... "等等,第一个闭包名呢?"

与之前相比,Swift 在 5.3 中默认不显示第一个闭包参数名,后面的闭包均显示出了参数名。对此,苹果的说法为:这样的写法无伤大雅,因为含有多尾随闭包的方法一般第一个为最主要的闭包,其他闭包都是可选的。这意思是默认所有开发者都有良好的开发规范,如方法名,参数名等等。

对于晚期强迫症有个好消息,第一个尾随闭包的参数名是可选的,开发者可以自己加上名称,以 UIView.animte 为例:

    // 单个闭包
    UIView.animate(withDuration: 0.3, animations: {() in
        // ...
    })

    // 多个闭包
    UIView.animate(withDuration: 0.3, animations: { () in
        // ...
    }, completion: { (finish) in
        // ...
    })

但为了加上第一个闭包的参数,几乎将默认的调用结构重写一遍,这样的代价是否值得?是否可以使用一个中间闭包来包含所有尾随闭包?又或者自定义一个链式语法的扩展链接所有尾随闭包?

中间闭包增大了方法的复杂性,而链式调用扩展来连接所有闭包是对尾随闭包的二次封装,SE-0279 中的设计原理部分可以看到多尾随闭包底层的实现机制,仅为了一个闭包名进行二次封装是否值得?实现出的效果风格也异于系统。

扩展:向后搜索匹配与向前搜索匹配

对于多尾随闭包,5.3之前和之后,一个是匿名闭包放最末尾,一个是匿名闭包在最前。 当匿名闭包与剩余未匹配参数不是1对1的关系时 Swift 又是如何匹配匿名闭包的?

例如下面代码,以下谁将持有闭包?

func test(a: () -> Int = { 1 }, b: Any? = nil) {}
test { 2 }
// a 与 b 谁将持有传入的闭包参数?

test 函数有 ab 两个参数,两者均拥有默认参数。运行后发现 b 持有了传入的闭包。为什么会这样?

SE-0279 的设计原理中提到,Swift 匹配多尾随闭包最开始使用的是 “backwards scan”(向后扫描/逆向扫描)来匹配匿名闭包。

通过例子了解下向后扫描/逆向扫描是个什么东西:

typealias RJBlock = ()->Int
func test(a: RJBlock? = nil, b: RJBlock? = nil, c:RJBlock? = nil, d:RJBlock? = nil) {
    print(a,b,c,d)
}

test(a: {1}, b: {2}, c: {3}, d: {4})    // 1
test(b: {100})                        // 2
test{ 100 }                          // 3
test(a: nil) { 100 }                  // 4
test(b: nil) { 100 }                  // 5
test {200} c: { 100 }                 // 6

持有结果如下:

  1. 传入了所有闭包且指定了对应的参数,运行正常。
  2. 只传入了一个闭包并指定其为b的参数,其余参数使用默认值,运行正常。
  3. 传入了一个匿名闭包,从运行结果来看,其被 d 所持有
  4. 传入了一个闭包给予 a 与一个尾随匿名闭包,匿名闭包被 d 所持有
  5. 传入了一个闭包给予 b 与一个尾随匿名闭包, 匿名闭包被 d 所持有
  6. 传入了一个指定参数的尾随闭包c,第一个参数为匿名闭包,匿名闭包被a所持有

测试的过程中可以看到3,4,5用例均显示了警告信息:

Backward matching of the unlabeled trailing closure is deprecated; label the argument with 'XXX' to suppress this warning
提示我们不推荐使用未标记的尾随闭包来进行“向后匹配”。前面的举例是因为 b 参数为 Any 类型,所以编译器未提示警告。

多尾随闭包中匿名闭包匹配是通过对参数执行向后扫描完成的。使用标签匹配所有带标签的尾随闭包,然后从匹配到的最后一个标记参数对匿名尾随闭包执行扫描。所以,在4,5用例中,指定了部分参数名,尾随闭包从最后一个参数开始“向后扫描”,而d就是第一个被扫描的参数,扫描类型结果匹配,所以匿名的尾随闭包就赋值给了d

等等···在第六个用例,指定了参数c,逆向搜索不应该是从d开始么,就算是从C开始,也应该是b啊,怎么匿名闭包赋值给了a?还是因为匿名闭包非末尾闭包,匹配方式有所不同?这与"向后扫描"本身缺陷有关。

SE-0286中,苹果注意到了多尾随闭包“向后扫描匹配”所衍生的问题:

向后扫描匹配规则使得使用尾部闭包(尤其是多个尾随闭包)编写良好的 API 变得困难。

文档中苹果举了以下例子:

class func animate(
    withDuration duration: TimeInterval, 
    animations: @escaping () -> Void, 
    completion: ((Bool) -> Void)? = nil
)

UIView.animate(withDuration: 0.3) {
  self.view.alpha = 0
}

使用“向后搜索匹配”传入的匿名闭包将被completion持有,这明显不是我们想要的结果,为解决“向后搜索匹配”的问题,,苹果提出了“向前搜索匹配”了。上面的例子在“向前搜索匹配”下等价于:

UIView.animate(withDuration: 0.3, animations: {
  self.view.alpha = 0
})

使用“向前搜索匹配”时,匿名闭包将从未匹配的参数从前向后进行匹配,用例中匹配到的第一个参数为animations

接下来我们尝试一下参数默认值对匹配的影响:

// 删除参数b的默认值,匿名函数将优先匹配 b
func test(a: RJBlock? = nil, b: RJBlock?, c:RJBlock? = nil, d:RJBlock? = nil) {}

无论是向前搜索匹配还是向后搜索匹配都会优先匹配第一个找到的无默认值的同类型参数,当全部都有默认值时则匹配第一个类型相匹配的参数(此处可以扩展闭包与方法类型的匹配)。那么何时使用向前匹配,何时又使用向后匹配

对此,SE-0286 有这样一段描述:

“Swift会使用两种方法进行匹配,如果两者都成功了,由于兼容性考虑将首选向后扫描匹配”
”向后匹配将会在 Swift6 中移除“

总结

  1. Swift 在 5.3 版本中,加入了显示末尾闭包的参数名,默认隐藏了首个尾随闭包的参数名。
  2. 在部分多尾随闭包的场景下,不同Swift版本下将会有不同的匹配结果。
  3. 向后匹配将会在 Swift6 中移除,在多个匿名闭包的地方要留意使用了向后搜索匹配的情况。
  4. 与其总让 Swift 去采用向前/向后匹配,不如老老实实按规范把参数名写上。

最新更新以及更多的深入剖析与尝试请查看官方文档SE-0279SE-0286

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