译自 www.hackingwithswift.com/articles/21…
更多内容,欢迎关注公众号 「Swift花园」
喜欢文章?不如来个 🔺💛➕三连?关注专栏,关注我 🚀🚀🚀
Swift 5.3 有不少变化,这其中包括多模式 catch 语句,多拖尾闭包,以及 Swift Package Manager 的一些重要改变。
本文会带你浏览一些主要的变化,同时提供参考代码,以便你可以自行尝试。以下是要介绍的新特性的清单:
- 多模式 catch 语句
- 多拖尾闭包
- 为枚举自动生成的 Comparable 实现
- self. 书写省略
- 基于类型的程序入口
- 基于上下文泛型声明的
where
语句 - 枚举的 cases 可以作为 protocol witnesses
- 重新提炼的
didSet
语义 - 新的 Float16 类型
- Swift Package Manager 支持二进制依赖,资源等更多类型
多模式 catch 语句
SE-0276 引入了一个可以在单个 catch
块中捕获多个错误 case 的特性,这能让我们免除错误处理时的重复代码。
例如,下面的代码用枚举定义了错误的两种情况:
enum TemperatureError: Error {
case tooCold, tooHot
}
当我们读取到温度时,既可以抛出两种错误中的某一个,也可以返回 “OK”:
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 允许我们用逗号分隔来表示我们要以相同方式处理 tooHot
和 tooCold
。
do {
let result = try checkReactorOperational()
print("结果: \(result)")
} catch TemperatureError.tooHot, TemperatureError.tooCold {
print("关闭反应堆")
} catch {
print("未知错误")
}
处理的 case 可以是任意数量的。
多拖尾闭包
SE-0279 引入了多拖尾闭包,这使得调用包含多个闭包的函数可以更简单地实现。
这个特性在 SwiftUI 中非常受欢迎。原来形如下面这样的代码:
struct OldContentView: View {
@State private var showOptions = false
var body: some View {
Button(action: {
self.showOptions.toggle()
}) {
Image(systemName: "gear")
}
}
}
可以被改写成:
struct NewContentView: View {
@State private var showOptions = false
var body: some View {
Button {
self.showOptions.toggle()
} label: {
Image(systemName: "gear")
}
}
}
理论上并不要求 label:
要跟在前一个闭包的 }
后面,所以你甚至可以像下面这样书写:
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
实现,代码如下:
enum Size: Comparable {
case small
case medium
case large
case extraLarge
}
然后我们就可以创建两个这个枚举的实例,并且用 <
进行比较:
let shirtSize = Size.small
let personSize = Size.large
if shirtSize < personSize {
print("T恤 太小了!")
}
自动生成的实现,也能很好地适应枚举的 Comparable
关联值。例如,假设我们有一个枚举,描述了某个队伍获取世界杯冠军的次数,代码可以这样实现:
enum WorldCupResult: Comparable {
case neverWon
case winner(stars: Int)
}
然后我们用不同的值来创建枚举的不同实例,并且让 Swift 对它们进行排序:
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
是多余的。
例如,之前我们需要把代码写成下面这样:
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.
:
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 的文件,然后把启动代码放在里面:
struct OldApp {
func run() {
print("Running!")
}
}
let app = OldApp()
app.run()
Swift 会自动把 main.swift 看作最顶层的代码,创建 App
实例并且运行。即便在 SE-0281 之后这个做法都一直被延续,但现在你可以干掉 main.swift 了,转而使用 @main
属性来标记某个包含静态 main
方法的结构体或者类,让它充当程序入口:
@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
,可以压栈,出栈元素:
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
协议的时候才能使用:
extension Stack {
func sorted() -> [Element] where Element: Comparable {
array.sorted()
}
}
枚举的 cases 可以作为 protocol witnesses
SE-0280 使得枚举可以参与 protocol witness matching,这是一种表述我们可以更容易地匹配协议要求的技术方式。
例如,你可以编写代码处理各种类型的数据,但是假如数据不见了怎么办呢?当然,你可以借助空合运算符,每次都提供一个默认值。不过。你可以借助协议来要求默认值,然后让各种类型遵循这个协议:
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
枚举,它能接收像素值,厘米值,或者是系统的默认值:
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
即可,方式如下:
didSet {
_ = oldValue
}
新的 Float16 类型
SE-0277 引入了一个新的半精度浮点类型,Float16
。这个精度在图像编程和机器学习中十分常见。
新类型和 Swift 原来的其他类型相似:
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)
。
我的公众号 这里有Swift及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~