Swift 5.1 新特性

Posted by Phillip Song on 2019-08-06

跟 Swift 5.0 一样, 5.1 的一个重要特性就是模块的稳定性,这使我们可以使用第三方库的同时无需担心它们使用的 Swift 编译器版本。这听起来类似我们在 Swift 5.0 版本中获得的 ABI 稳定性,但是有一个细微的差别: ABI 稳定性解决了 Swift 的运行时差异,而模块稳定性解决了编译时的差异。

除了这个重要的里程碑之外,我们还获得了许多重要的语言的改进,在本文中,我就介绍它们并提供代码示例,以便你可以看到它们的运行情况。

PS: 如果你错过了 Swift 5.1 的所有新功能,请从这里开始: Swift 5.0 中的新功能

  • 对成员逐一构造器的大量改进

SE-0242 对 Swift 最常用的功能之一进行了重大改进:结构体的成员构造器。

在之前版本的 Swift 中,会自动创建一个成员构造器来接受与结构属性匹配的参数,如下所示:

1
2
3
4
5
6
struct User {
var name: String
var loginCount: Int = 0
}

let piper = User(name: "Piper Chapman", loginCount: 0)

成员构造器在 Swift 5.1 中得到了增强,因此成员构造器对具有默认参数值的属性使用默认参数值。在 User 结构体中,我们给 loginCount 一个默认值 0,这意味着我们可以指定它或将它留给成员初始化器:

1
2
let gloria = User(name: "Gloria Mendoza", loginCount: 0)
let suzanne = User(name: "Suzanne Warren")

这让我们避免重复代码,这总是受欢迎的。

  • 单表达式函数的隐式返回

SE-0255 删除了语言中一个小而重要的不一致性: 有返回值的单表达式函数现在可以删除 return 关键字, Swift 将隐式理解它。

在以前版本的 Swift 中,有返回值的单行闭包可以省略 return 关键字,因为闭包里唯一的代码行必须是返回值的代码。所以,这两段代码是相同的:

1
2
let doubled1 = [1, 2, 3].map { $0 * 2 }
let doubled2 = [1, 2, 3].map { return $0 * 2 }

在 Swift 5.1 中,这种行为现在已经扩展到函数: 如果它们包含单个表达式 - 实际上是一段值计算的代码,那么你可以省略 return 关键字,如下所示:

1
2
3
func double(_ number: Int) -> Int {
number * 2
}

这可能会让一些人一开始多看一眼,但我相信随着时间的推移会更习惯这种写法。

  • 全局的 Self

SE-0068 扩展了 Swift 对 Self 的使用,以便在类,结构体和枚举中使用时引用包含类型。这对于那些需要在运行时确定确切类型的动态类型特别有用。

例如,请考虑以下代码:

1
2
3
4
5
6
7
8
9
class NetworkManager {
class var maximumActiveRequests: Int {
return 4
}

func printDebugData() {
print("Maximum network requests: \(NetworkManager.maximumActiveRequests).")
}
}

NetworkManager 声明了的静态 maximumActiveRequests 属性,并添加了 printDebugData() 方法来打印静态属性。现在工作正常,但是当 NetworkManager 被子类化时,事情就变得更复杂:

1
2
3
4
5
class ThrottledNetworkManager: NetworkManager {
override class var maximumActiveRequests: Int {
return 1
}
}

该子类更改了 maximumActiveRequests, 以便一次只允许一个请求,但如果我们调用 printDebugData(),它将从其父类打印出该值:

1
2
let manager = ThrottledNetworkManager()
manager.printDebugData()

这应该打印出1而不是4,这就是 SE-0068 带来的改变:我们现在可以使用 Self 来引用当前类型。所以,我们可以将 printDebugData() 重写为:

1
2
3
4
5
6
7
8
9
class ImprovedNetworkManager {
class var maximumActiveRequests: Int {
return 4
}

func printDebugData() {
print("Maximum network requests: \(Self.maximumActiveRequests).")
}
}

这意味着 Self 的工作方式与在早期 Swift 版本中的协议相同。

  • 不透明返回类型

SE-0244 将不透明类型的概念引入 Swift。不透明类型是指我们被告知对象的功能而不知道对象具体是什么类型。

乍一看,这听起来很像是协议,但不透明的返回类型更进一步的采用了协议的概念,因为他们能够与关联类型进行合作,它们每次都需要在内部使用相同的类型,并且允许我们隐藏实现细节。

举个例子,如果我们想从 Rebel 基地发射不同种类的战士,我们可以会编写如下代码:

1
2
3
4
5
6
7
8
protocol Fighter { }
struct XWing: Fighter { }

func launchFighter() -> Fighter {
return XWing()
}

let red5 = launchFighter()

无论谁调用该功能都知道它将返回某种 Fighter,但不知道具体是什么。因此,我们可以添加 struct YWing: Fighter {} 或其他类型,并返回其中任何类型。

但是有一个问题:如果我们想检查一个特定的战斗机是否是 Red 5 怎么办?你可能认为解决方法是使 Fighter 符合 Equatable 协议,因此我们可以使用 ==。但是,只要你这样做,Swift 就会为 launchFighter 函数抛出一个特别可怕的错误: “Protocol Fighter can only be used as a generic constraint because it has Self or associated type requirements.”。

这里打击我们的就是这个错误的 Self 部分。Equatable 协议必须比较它自身(Self)的两个实例以查看它们是否相同,但是 Swift 不能保证两个相等的东西是远程相同的 - 比如我们可能将一个 Fighter 与一个整数数组进行比较。

不透明类型解决了这个问题,因为即使我们只看到正在使用的协议,Swift 编译器内部确切知道该协议实际解析的内容 - 它知道他是一个 XWing,一个字符串数组,或者其他什么。

要返回不透明类型,需要在协议名称前使用关键字 some:

1
2
3
func launchOpaqueFighter() -> some Fighter {
return XWing()
}

从调用者的角度来看,仍然可以获得 Fighter,这可能是 XWingYWing 或其他符合 Fighter 协议的对象。但是从编译器的角度来看,它确切的知道返回的内容,因此它可以确保我们正确的遵循所有的规则。

例如,考虑一个返回 some Equatable 的函数

1
2
3
func makeInt() -> some Equatable {
Int.random(in: 1...10)
}

当我们调用它时,我们所知道的是它是某种 Equatable 值, 但是如果调用它两次,那么我们就可以比较这两个调用的结果,因为 Swift 肯定知道他将是相同的底层类型。

1
2
3
let int1 = makeInt()
let int2 = makeInt()
print(int1 == int2)

如果我们有第二个返回一些 Equatable 的函数,则不是这样,如下所示:

1
2
3
func makeString() -> some Equatable {
"Red"
}

从我们的角度来看,即使都返回了 Equatable 类型,我们可以比较两次调用 makeString() 的结果或两次调用 makeInt() 的结果,但 Swift 不会让我们将 makeString() 的返回值与 makeInt() 的返回值进行比较,因为它知道比较一个字符串和一个整数没有任何意义。

这里一个重要的条件是具有不透明返回类型的函数必须始终返回一个特定类型。 例如,如果我们尝试使用 Bool.random() 随机启动 XWingYWing, 那么 Swift 将拒绝构建我们的代码,因为编译器无法再区别将要返回的类型。

你可能会认为”如果我们总是需要返回相同的类型,为什么不将函数写成 func launchFighter() -> XWing?“。 虽然这可能有时可以,但它会产生新的问题,例如:

  • 我们最终得到的类型并不想暴露出去。例如,如果我们使用 someArray.lazy.drop {...},我们会收到一个 LazyDropWhileSequence - 一个来自 Swift 标准库的专用且特定的类型。我们真正关心的是这个东西是一个序列,我们不需要知道 Swift 的内部结构是如何工作的。
  • 我们失去了之后改变主意的能力。使 launchFighter() 只返回 XWing 意味着我们将来不能切换到另一种类型,并且意识到迪士尼依赖星球大战玩具销售会有多大问题!通过返回不透明类型,我们今天可以返回 XWing,然后可以在短时间转移到 BWings - 我们只能在代码的任何给定构建中返回一种,但我们仍然可以灵活地改变主意。

在某些方面,所有这些听起来都类似与解决了 “Self or associated type requirements” 问题的范型。范型允许我们编写如下代码:

1
2
3
4
5
6
7
8
9
10
protocol ImperialFighter {
init()
}

struct TIEFighter: ImperialFighter { }
struct TIEAdvanced: ImperialFighter { }

func launchImperialFighter<T: ImperialFighter>() -> T {
return T()
}

这里定义了一个新协议以及两个满足协议的结构体,而 launchImperialFighter() 函数用来使用它们。但是,这里的区别在于,launchImperialFighter() 的调用者可以选择它们获得的战斗机类型,如下:

1
2
let fighter1: TIEFighter = launchImperialFighter()
let fighter2: TIEAdvanced = launchImperialFighter()

如果你希望调用者能够选择它们的数据类型,那么范型可以很好的解决问题,但如果你希望函数决定返回类型,那么它们就会失败。

因此,不透明的结果类型允许我们做几件事:

  • 我们的函数决定返回什么类型的数据,而不是这些函数的调用者。
  • 我们不需要担心 Self 或相关类型要求,因为编译器确切的知道内部类型。
  • 无论何时我们需要,都可以改变主意。
  • 我们不会将私有内部类型暴露给外部。

  • 静态和类下标

SE-0254 增加了将下标标记为静态的功能,这意味着它们适用于类型而不是类型的实例。

当在该类型的所有实例之间共享一组值时,将使用静态属性和方法。例如,如果你有一个集中存储应用程序设置,则可以编写如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public enum OldSettings {
private static var values = [String: String]()

static func get(_ name: String) -> String? {
return values[name]
}

static func set(_ name: String, to newValue: String?) {
print("Adjusting \(name) to \(newValue ?? "nil")")
values[name] = newValue
}
}

OldSettings.set("Captain", to: "Gary")
OldSettings.set("Friend", to: "Mooncake")
print(OldSettings.get("Captain") ?? "Unknown")

将字典包装在一个枚举类型中意味着我们可以更仔细的控制访问,并且没有任何 case 的枚举意味着我们不能尝试初始化类型 - 我们不能创建各种设置的实例。

使用 Swift 5.1,我们现在可以使用静态下标,允许我们将代码重写为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public enum NewSettings {
private static var values = [String: String]()

public static subscript(_ name: String) -> String? {
get {
return values[name]
}
set {
print("Adjusting \(name) to \(newValue ?? "nil")")
values[name] = newValue
}
}
}

NewSettings["Captain"] = "Gary"
NewSettings["Friend"] = "Mooncake"
print(NewSettings["Captain"] ?? "Unknown")

像这样的自定义下标总是可以用于类型的实例,这种改进使得静态或类下标称为可能。

  • Warnings for ambiguous none cases

Swift 的 optional 类型的实现是 somenone 两种 case 的枚举。如果我们创建一个没有 case 的枚举,然后将它包装在一个可选项中,就会产生混淆的可能性。

例如:

1
2
3
4
enum BorderStyle {
case none
case solid(thickness: Int)
}

用作非可选项,这一直是明确的:

1
2
let border1: BorderStyle = .none
print(border1)

这将打印出 “none”。 但是如果我们为枚举使用一个可选类型 - 如果我们不知道使用什么边框样式,那么我们就遇到了问题:

1
2
let border2: BorderStyle? = .none
print(border2)

这会打印出 “nil”,因为 Swift 假定 .none 表示可选项为空,而不是可选的值为 BorderStyle.none

在 Swift 5.1 中,这种混乱现在会打印出一个警告:“Assuming you mean Optional.none; did you mean BorderStyle.none instead? “ 这可以避免破坏源兼容性的错误,但至少告诉开发人员他们的代码可能并不完全意味着他们的想法。

  • Matching optional enums against non-optionals

Swift 一直很聪明,可以处理 switch/case 模式匹配中的字符串与整数的可选和非可选值之间的匹配,但在 Swift 5.1 之前没有扩展到枚举中。

好吧,在 Swift 5.1 中我们现在可以使用 switch/case 模式匹配来匹配可选和非可选,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
enum BuildStatus {
case starting
case inProgress
case complete
}

let status: BuildStatus? = .inProgress

switch status {
case .inProgress:
print("Build is starting…")
case .complete:
print("Build is complete!")
default:
print("Some other build status")
}

Swift 能够直接将可选枚举与非可选枚举 case 进行比较,因此代码将打印出 “Build is starting…”。

  • Ordered collection diffing

SE-0240 引入了在有序集合之间计算和应用差异的能力。对于在表视图中具有复杂集合的开发人员来说,这可能特别有趣,他们希望使用动画平滑地添加和删除大量项目。

基本原理很简单:Swift 5.1 为我们提供了一个新的 difference(from:) 方法,它计算两个有序集合之间的差异 - 要删除的项目和要插的项目。它可以与包含 Equatable 元素的任何有序集合一起使用。

为了证明这一点,我们可以创建一个存储分数(score)的数组,计算从一个到另一个的差异,然后循环这些差异并应用到每个分数以使我们的两个集合相同。

注意: 由于 Swift 在 Apple 的操作系统中发布,因此必须使用 #available 检查这样的新功能,以确保代码在包含新功能的操作系统上运行。对于将在未来的某个时间点,发布在未知地操作系统中的功能,特殊版本号 “9999” 用于表示“我们还不知道实际的数字是什么”。

下面是代码实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var scores1 = [100, 91, 95, 98, 100]
let scores2 = [100, 98, 95, 91, 100]

if #available(iOS 9999, *) {
let diff = scores2.difference(from: scores1)

for change in diff {
switch change {
case .remove(let offset, _, _):
scores1.remove(at: offset)
case .insert(let offset, let element, _):
scores1.insert(element, at: offset)
}
}

print(scores1)
}

对于更高级的动画,你可以使用更改的第三个值 associatedWith。因此,不是使用 .insert(let offset,let element,_),而是使用 .insert(let offset,let element,let associatedWith) 来代替。 这使你可以同时跟踪成对地变化:在集合中向下移动两个项目是删除然后插入,但 associatedWith 值将这两个更改绑定在一起,因此将其视为移动。

您可以使用新的 apply() 方法应用整个集合,而不是手动应用更改,如下所示:

1
2
3
4
if #available(iOS 9999, *) {
let diff = scores2.difference(from: scores1)
let result = scores1.applying(diff) ?? []
}
  • Creating uninitialized arrays

SE-0245 为数组引入了一个新的构造器,它不会使用默认值来预填充值。这是以前作为私有 API 提供的,这意味着 Xcode 不会在代码补全时将其列出,当然如果你愿意,你仍然可以使用它 - 如果你愿意为此承担将来撤销相关 API 的风险。

要使用这个构造器,需要告诉它想要的容量,然后提供一个闭包来填充你需要的值。你提供的闭包将被赋予一个不安全的可变缓冲区指针,你可以在其中赋予你的值,以及第二个 inout 的参数,可让你知道实际使用的值的数量。

例如,我们可以创建一个包含10个随机整数的数组:

1
2
3
4
5
6
7
let randomNumbers = Array<Int>(unsafeUninitializedCapacity: 10) { buffer, initializedCount in
for x in 0..<10 {
buffer[x] = Int.random(in: 0...10)
}

initializedCount = 10
}

这里有一些规则:

  • 你不需要使用完所有的容量,但是不可以超过容量。因此,如果要求容量为10,则可以将 initializedCount 设置为 0 到 10, 而不是 11。如果不初始化最终存在于数组中的元素 - 例如,如果将 initializedCount 设置为 5 但实际上没有为元素 0 到 4 提供值 - 那么它们很可能会填充随机数据。这是一个坏主意。
  • 如果未设置 initializedCount. 则它将为0,因此你指定的任何数据都将丢失。

现在,我们可以使用 map() 重写上面的代码,如下所示:

1
let randomNumbers2 = (0...9).map { _ in Int.random(in: 0...10) }

这虽然更易读,但效率较低:它创建一个范围,创建一个新的空数组,将其调整到正确的数量。在范围内循环,并为每个范围项调用一次闭包。

More to come!

Swift 5.1 仍处于开发阶段,尽管 Swift 本身的最终分支已经过去,但仍有一些空间可以看到其他一些相关项目的变化。

同样,这里的重要特征是模块稳定性,我知道团队正在努力做到这一点。他们没有宣布发布日期,尽管他们说由于 Swift 5.0 需要“不寻常的关注和注意”而导致5.1“开发时间明显缩短” - 我猜我们会在 WWDC19 上看到测试版,但显然这不是特定日期的匆忙。

值得特别提及的一件事是,这里列出的两个变化并没有作为 Swift Evolution 的结果引入。相反,更改 - “Warnings for ambiguous none cases” 和 “Matching optional enums against non-optionals” - 被视为错误并快速纠正。

这些都是 Swift 的高品质生活改进,但我之所以特别称它们是因为它们都是由社区贡献者修改的:Suyash Srijan。很高兴看到 Swift 的发展继续超越 Apple,而 Suyash 在这两个高度可见的功能上的工作正在帮助 Swift 更轻松,更加一致。

最重要的是,这个含糊不清的枚举错误被归档为一个初学者错误,这是 Swift 团队专门挑选出来的,以便让人们更容易开始贡献。如果您想亲自探索当前的入门错误,甚至可能会尝试修复它,请访问 http://bit.ly/starterbugs。