Swift 5.3 新特性

swift-evolution-4

Swift 5.3 有不少变化,这其中包括多模式 catch 语句,多拖尾闭包,以及 Swift Package Manager 的一些重要改变。

本文会带你浏览一些主要的变化,同时提供参考代码,以便你可以自行尝试。以下是要介绍的新特性的清单:

  • 多模式 catch 语句
  • 多拖尾闭包
  • 为枚举自动生成的 Comparable 实现
  • self. 书写省略
  • 基于类型的程序入口
  • 基于上下文泛型声明的 where 语句
  • 枚举的 cases 可以作为 protocol witnesses
  • 重新提炼的 didSet 语义
  • 新的 Float16 类型
  • Swift Package Manager 支持二进制依赖,资源等更多类型

多模式 catch 语句

SE-0276 引入了一个可以在单个 catch 块中捕获多个错误 case 的特性,这能让我们免除错误处理时的重复代码。

例如,下面的代码用枚举定义了错误的两种情况:

1
2
3
enum TemperatureError: Error {
case tooCold, tooHot
}

当我们读取到温度时,既可以抛出两种错误中的某一个,也可以返回 “OK”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func getReactorTemperature() -> Int {
90
}

func checkReactorOperational() throws -> String {
let temp = getReactorTemperature ()

if temp < 10 {
throw TemperatureError.tooCold
} else if temp > 90 {
throw TemperatureError.tooHot
} else {
return "OK"
}
}

在捕获错误的环节,SE-0276 允许我们用逗号分隔来表示我们要以相同方式处理 tooHottooCold

1
2
3
4
5
6
7
8
do {
let result = try checkReactorOperational ()
print("结果: \(result)")
} catch TemperatureError.tooHot, TemperatureError.tooCold {
print("关闭反应堆")
} catch {
print("未知错误")
}

处理的 case 可以是任意数量的。

多拖尾闭包

SE-0279 引入了多拖尾闭包,这使得调用包含多个闭包的函数可以更简单地实现。

这个特性在 SwiftUI 中非常受欢迎。原来形如下面这样的代码:

1
2
3
4
5
6
7
8
9
10
11
struct OldContentView: View {
@State private var showOptions = false

var body: some View {
Button(action: {
self.showOptions.toggle ()
}) {
Image(systemName: "gear")
}
}
}

可以被改写成:

1
2
3
4
5
6
7
8
9
10
11
struct NewContentView: View {
@State private var showOptions = false

var body: some View {
Button {
self.showOptions.toggle ()
} label: {
Image(systemName: "gear")
}
}
}

理论上并不要求 label: 要跟在前一个闭包的 } 后面,所以你甚至可以像下面这样书写:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct BadContentView: View {
@State private var showOptions = false

var body: some View {
Button {
self.showOptions.toggle ()
}

label: {
Image(systemName: "gear")
}
}
}

不过,我会建议你当心代码的可读性 —— 像上面的 label 那样的代码块,在 Swift 里看起来更像是标签化的代码块,而不是 Button 构造器的第二个参数。

** 注:** 有关 Swift 的多拖尾闭包特性的讨论非常热烈。我想提醒大家的是,像这种类型的语法变动一开始看起来可能会有点别扭,我们需要耐心,给它时间,在实践中体会它带来的结果。

为枚举自动生成的 Comparable 实现

SE-0266 使得我们可以为枚举生成 Comparable 实现,同时不要求我们声明关联值,或者要求关联值本身必须是 Comparable 的。这个特性让我们可以在同类型的枚举之间用 <> 和类似的比较操作符来进行比较。

例如,假设我们有一个枚举,它描述了衣服的尺寸,我们可以要求 Swift 为它自动生成 Comparable 实现,代码如下:

1
2
3
4
5
6
enum Size: Comparable {
case small
case medium
case large
case extraLarge
}

然后我们就可以创建两个这个枚举的实例,并且用 < 进行比较:

1
2
3
4
5
6
let shirtSize = Size.small
let personSize = Size.large

if shirtSize < personSize {
print("T 恤 太小了!")
}

自动生成的实现,也能很好地适应枚举的 Comparable 关联值。例如,假设我们有一个枚举,描述了某个队伍获取世界杯冠军的次数,代码可以这样实现:

1
2
3
4
enum WorldCupResult: Comparable {
case neverWon
case winner (stars: Int)
}

然后我们用不同的值来创建枚举的不同实例,并且让 Swift 对它们进行排序:

1
2
3
4
5
6
7
8
let americanMen = WorldCupResult.neverWon
let americanWomen = WorldCupResult.winner (stars: 4)
let japaneseMen = WorldCupResult.neverWon
let japaneseWomen = WorldCupResult.winner (stars: 1)

let teams = [americanMen, americanWomen, japaneseMen, japaneseWomen]
let sortedByWins = teams.sorted ()
print(sortedByWins)

排序过程会把未获得世界杯冠军的队伍放在前面,然后是日本女子队,再然后是美国女子队 —— 两组 winner 的队被认为是大于两组 neverWon 的队,而 winner (stars: 4) 被认为是大于 winner (stars: 1)

self. 书写省略

SE-0269 使得我们可以在一些不必要的地方省略 self 。在这个改变之前,我们需要在所有的闭包当中对引用 self 的属性或者方法冠以 self.,以便显式地明确语义。但有的时候由于闭包不可能产生引用循环,self 是多余的。

例如,之前我们需要把代码写成下面这样:

1
2
3
4
5
6
7
8
9
10
11
struct OldContentView: View {
var body: some View {
List(1..<5) { number in
self.cell (for: number)
}
}

func cell(for number: Int) -> some View {
Text("Cell \(number)")
}
}

self.cell (for:) 的调用不会产生引用循环,因为它是在结构体内使用。多亏了 SE-0269,上面的代码现在可以免去 self.

1
2
3
4
5
6
7
8
9
10
11
struct NewContentView: View {
var body: some View {
List(1..<5) { number in
cell (for: number)
}
}

func cell(for number: Int) -> some View {
Text("Cell \(number)")
}
}

这个特性对于大量使用闭包的框架非常有用,包括 SwiftUI 和 Combine。

基于类型的程序入口

SE-0281 引入了一个新的 @main 属性,它可以让我们声明程序的入口。这个特性使得我们可以精确地控制程序启动时要执行的代码,对于命令行程序尤其有帮助。

例如,当我们创建一个终端应用时,我们必须创建一个叫 main.swift 的文件,然后把启动代码放在里面:

1
2
3
4
5
6
7
8
struct OldApp {
func run() {
print("Running!")
}
}

let app = OldApp()
app.run ()

Swift 会自动把 main.swift 看作最顶层的代码,创建 App 实例并且运行。即便在 SE-0281 之后这个做法都一直被延续,但现在你可以干掉 main.swift 了,转而使用 @main 属性来标记某个包含静态 main 方法的结构体或者类,让它充当程序入口:

1
2
3
4
5
6
@main
struct NewApp {
static func main() {
print("Running!")
}
}

上面的代码所在的程序运行时,Swift 会自动调用 NewApp.main () 来启动程序流程。

新的 @main 属性对于 UIKit 和 AppKit 开发者来说可能有点属性,因为我们正是用 @UIApplicationMain@NSApplicationMain 来标记 app 代理的。

不过,使用 @main 的时候有一些注意事项:

  • 已经有 main.swift 文件的 app 不能使用这个属性
  • 不能有一个以上的 @main 属性
  • @main 属性只能用在最顶层的类型上 —— 这个类型不继承自任何其他类

基于上下文泛型声明的 where 语句

SE-0267 引入一个新特性,你可以给泛型类型或者扩展添加带有 where 语句限定的函数。

例如,我们创建了一个简单的 Stack,可以压栈,出栈元素:

1
2
3
4
5
6
7
8
9
10
11
struct Stack<Element> {
private var array = [Element]()

mutating func push(_ obj: Element) {
array.append (obj)
}

mutating func pop() -> Element? {
array.popLast ()
}
}

借助 SE-0267,我们现在可以添加一个 sorted () 方法给这个 Stack,并且要求这个方法只有在 Stack 的泛型参数 Element 遵循 Comparable 协议的时候才能使用:

1
2
3
4
5
extension Stack {
func sorted() -> [Element] where Element: Comparable {
array.sorted ()
}
}

枚举的 cases 可以作为 protocol witnesses

SE-0280 使得枚举可以参与 protocol witness matching,这是一种表述我们可以更容易地匹配协议要求的技术方式。

例如,你可以编写代码处理各种类型的数据,但是假如数据不见了怎么办呢?当然,你可以借助空合运算符,每次都提供一个默认值。不过。你可以借助协议来要求默认值,然后让各种类型遵循这个协议:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protocol Defaultable {
static var defaultValue: Self { get }
}

// 让整数有默认值 0
extension Int: Defaultable {
static var defaultValue: Int { 0 }
}

// 让数组有默认值空数组
extension Array: Defaultable {
static var defaultValue: Array { [] }
}

// 让字典有默认值空字典
extension Dictionary: Defaultable {
static var defaultValue: Dictionary { [:] }
}

SE-0280 使得我们能对枚举做出一样的控制。比如,你有一个 padding 枚举,它能接收像素值,厘米值,或者是系统的默认值:

1
2
3
4
5
enum Padding: Defaultable {
case pixels (Int)
case cm (Int)
case defaultValue
}

这样的代码在 SE-0280 之前是无法实现的 —— Swift 会抱怨 Padding 不满足协议。但是,如果你仔细琢磨一下,协议其实是满足的:我们需要一个静态的 defaultValue,它返回 Self,换言之,就是某个遵循协议的具体类型,而这正是 Padding.defaultValue 提供的。

重新提炼的 didSet 语义

SE-0268 调整了 didSet 属性观察者的工作方式,以便它们能更高效地工作。对于这个优化你不需要改动任何代码,自动获得一个小小的性能提升。

在内部,Swift 做出的改变是在设置新值时不再查询旧值。如果你不使用旧值,也没有设置 willSet,Swift 会即时修改数值。

假如你需要用到旧值,只需要引用 oldValue 即可,方式如下:

1
2
3
didSet {
_ = oldValue
}

新的 Float16 类型

SE-0277 引入了一个新的半精度浮点类型,Float16。这个精度在图像编程和机器学习中十分常见。

新类型和 Swift 原来的其他类型相似:

1
2
3
4
let first: Float16 = 5
let second: Float32 = 11
let third: Float64 = 7
let fourth: Float80 = 13

Swift Package Manager 支持二进制依赖,资源等更多类型

Swift 5.3 为 Swift Package Manager (SPM) 带来了很多提升,恕我不能在这里一一举例。不过我们可以讨论一下 SPM 有哪些变化以及为什么会有这些变化。

首先,SE-0271 (Package Manager Resources) 使得 SPM 能包含诸如图片,音频,JSON 等类型的资源。这个机制可不只是把文件拷进最终的 app bundle 这么简单 —— 举个例子,我们可以应用一个自定义处理步骤到我们的 assets,比如为 iOS 优化图片。为此,新增的 Bundle.module 属性就是用来在运行时访问这些 assets 的。SE-0278 (Package Manager Localized Resources) 进一步支持了资源的本地化版本,例如提供适用某个国家的图片。

其次,SE-0272 (Package Manager Binary Dependencies) 使得 SPM 可以使用二进制包。这意味着像 Firebase 这样的闭源 SDK 现在也可以通过 SPM 集成了。

再次,SE-0273 (Package Manager Conditional Target Dependencies) 可以让我们指定为特定平台和配置使用依赖。例如,我们可能在为 Linux 平台编译时,额外需要某些特定的框架,或者我们在本地测试的时候需要一些依赖调试用的框架。

值得一提的是,SE-0271 的 “Future Directions” 一节中提到了对资源的安全访问 —— 这意味着像 Image ("avatar") 这样的代码之后会变成 Image (module.avatar)