Swift 5.3 新特性精讲(1):多尾闭包,一个不能自洽的特性

4,898 阅读6分钟

也许是作为争议最大的特性之一,多尾闭包这个特性被纳入 Swift 5.3。为什么会有那么大的争议呢?听我慢慢道来。

单尾闭包

调用单个尾闭包的函数时有一种精简的写法:省去这个尾闭包的标签、并且把闭包放在圆括号外面。以下两种写法等价。

UIView.animate(withDuration: 0.25, animations: {
  // animation code
})

// 单个尾闭包的精简写法
UIView.animate(withDuration: 0.25) {
  // animation code
}

然而,Swift 5.3 之前如果有多个尾闭包的话,也只有最后一个闭包能被写成精简形式,这种写法苹果觉得不是太好看,因为一个闭包在圆括号内,另一个在外面。因此苹果的推荐做法是,碰到这种需要还是换成传统的写法吧。

// 写法 1(单尾闭包精简写法)
UIView.animate(withDuration: 0.25, animations: {
  // animation code
}) { completed in
  // completion code
}

// 写法 2(传统写法)
UIView.animate(withDuration: 0.25, animations: {
  // animation code
}, completion: { completed in
  // completion code
})

正因如此,给这次的争议埋下了隐患,因为这只是建议,并没有什么强制的措施,甚至编译器也不会给警告的。事实上,在 Xcode 12 之前,直接在最后一个闭包的代码提示上按回车,代码会被自动切换成第一种的形式。因此,第一种写法有非常多的存量代码。

多尾闭包

Swift 5.3 的时候,苹果“重新思考”了多尾闭包的场景,它给出了一种新的调用写法:

// 写法 3(多尾闭包精简写法)
UIView.animate(withDuration: 0.25) {
  // animation code
} completion: { completed in
  // completion code
}

这种新的写法,把最后的连续几个闭包参数都算作是尾闭包了,这些闭包现在都可以放在圆括号外面了,显得清爽了很多。

但是,这些尾闭包中的第一个闭包被强制省略,可以看到这里的 animations标签被强制省略,注意是强制哦!加上了是编译不过的。苹果给出的解释是:如果允许第一个尾闭包加上标签,那么开发者需要考虑是加好还是不加好,你看这会导致代码风格不一致,那么干脆禁了吧。

呵呵。一个多尾闭包的情况现在都有三种合法的写法了,你不在考虑增加第三种写法的时候考虑一下是不是会让开发者多一种选择的为难,却贴心的考虑到了“我就是要”增加的第三种写法里面让开发者少一点为难,谢谢你啊。

这个多尾闭包还有个特性是,除了第一个尾闭包不配拥有姓名之外,其余的尾闭包都得有姓名(标签)。所以搞笑的是:同样作为合法的写法,第一种写法省略了completion标签,而第三种写法省略了animations标签。这不是找骂么,对于 API 的设计者来说,一个清晰合理的调用是函数设计考量的重要一点,之前我要想到的是多闭包的情况下最后一个标签有可能被用户调用时候省略,但是现在你说,不不,第一个才会被强制省略?

苹果怎么办呢?只好在 API 设计规范里面说,大家现在开始要注意第一个尾闭包标签会被省略,要给之后的尾闭包起个好的标签名哦。

然而,这又解决了什么问题呢?考虑到源代码的兼容性,Swift 永远不可能去掉第一种调用方法,用户依旧可以使用第一种写法来使用多尾闭包的函数,所以上面所述的问题也会永远存在。苹果可以做的不过是在今后新的多尾闭包写法铺开之后,悄咪咪地给第一种写法加一些 warning 和自动转换的功能吧。

引起那么大争论,我想苹果现在很后悔为什么当初就不禁止第一种写法。如果当初禁止第一种写法,现在的改动应该会是一片叫好吧。

那么又是什么原因让苹果不惜背上被开发者喷的代价也要把这个特性加上呢?

哎,不是因为 SwiftUI 又是为什么呢?

苹果铺垫了那么多,说是解决一个通用的多尾闭包的问题,最后才把真正的意图亮出来,为了 SwiftUI 的 API。

嘤嘤嘤,没有多尾闭包,人家只能把 Section<Parent, Content, Footer> 设计成这个样子啦!

init(header: Parent, footer: Footer, content: () -> Content)

// 调用
Section(
  header: ...,
  footer: ...
) {
  // content
}

这里只有 content 才用上了 ViewBuilder 修饰的闭包 ViewBuilder 介绍headerfooter只能传入实例,导致用户必须写一个新的类型才行。而理想情况下,这三个参数都应该是 ViewBuilder 修饰的闭包:

init(content: () -> Content, header:() -> Parent, footer: () -> Footer)

// 调用
Section {
  // content
} header: {
  ...
} footer: {
  ...
}

并且 content 标签是会被默认省略的,所以应当放到第一个。

我们看一个新的 SwiftUI 的组件再来体会下:

struct Gauge<Label, CurrentValueLabel, BoundsLabel, MarkedValueLabels> 
where Label : View, CurrentValueLabel : View, BoundsLabel : View, MarkedValueLabels : View

init<V>(value: V, in: ClosedRange<V>, label: () -> Label, currentValueLabel: () -> CurrentValueLabel, minimumValueLabel: () -> BoundsLabel, maximumValueLabel: () -> BoundsLabel)

终于可以撒开了欢地用尾闭包了呢!

感谢wk_dev提醒,如果结合可空闭包默认值来看,问题还要复杂。下面这种以前想都不用想的函数调用,在你掌握了多尾闭包的知识后,你还知道test{}传入的闭包是给header还是footer吗?

func test(header: (()->Void)? = nil, footer: (()->Void)? = nil) // 定义
test {} // 调用

按照单尾闭包来看,毫无疑问headernilfooter{}

但是按照多尾闭包规则,我是不是可以理解成header{}footer因为默认值是nil被省略了呢?

事实上单尾闭包的规则起作用,也就是说传入的是 footer, 那有没有办法使得它应用多闭包的规则呢?比如:

test {} footer: nil // 不合法

这样的调用是不合法的,所以为了实现闭包可空,同时也是为了解决语义不清晰的二义性问题,也只好用重载实现可空。

init<V>(value: V, in: ClosedRange<V>, label: () -> Label)
init<V>(value: V, in: ClosedRange<V>, label: () -> Label, currentValueLabel: () -> CurrentValueLabel)
init<V>(value: V, in: ClosedRange<V>, label: () -> Label, currentValueLabel: () -> CurrentValueLabel, markedValueLabels: () -> MarkedValueLabels)
init<V>(value: V, in: ClosedRange<V>, label: () -> Label, currentValueLabel: () -> CurrentValueLabel, minimumValueLabel: () -> BoundsLabel, maximumValueLabel: () -> BoundsLabel)

结语

一门开源语言的进化动力,不能仅仅来源于一个私有框架。在去年为了 SwiftUI 引入大量新特性,并且最为关键的 FunctionBuilder 至今没有通过语言特性评审的情况下,苹果今年又一意孤行地引入多尾闭包这个争议特性,可谓是走了一条与开源精神背道而驰的路。

Swift 今年号称要推广到更多的平台,例如 Windows,这么维护开发者关系的话,很难说到时候有多少人会站在你这一边。

扫码下方二维码关注“面试官小健”