Swift 5 做了哪些改动?(最低支持 Xcode 10.2 beta)/ 3.24 updated

万众期待的 Swift 5 终于来了,苹果爸爸答应的 ABI 稳定也终于来了。

亲自尝试一下: Paul Hudson创建了一个Xcode Playground,显示了 Swift 5.0 中的新功能以及可以编辑的示例

Download Now

App 瘦身

新特性

Swift 应用程序不再包含用于 Swift 标准库的动态链接库和用于运行 iOS 12.2watchOS 5.2tvOS 12.2 的设备的构建变体中的 Swift SDK overlays。因此,当为 TestFlight 进行测试部署时,或者在为本地开发分发瘦身应用的 archive 包时,Swift 应用程序可以更小。

要对比 iOS 12.2 和 iOS 12.1 (或更早版本) 瘦身后 App 的文件大小差异,可以设置 App 的 deployment targetiOS 12.1 或更早版本,设置 scheme setGeneric iOS Device 并生成一个 App 的归档。在构建完成后,在 Archives organizer 选择中 Distribute App,然后选择 Development distribution。确保在 App Thinning 下拉菜单中选择一个特定设备,如 iPhone XS。当分发完成后,在新创建的文件夹下打开 App Thinning Size Report。iOS 12.2 系统的变体将小于 iOS 12.1 及更早的系统的变体。确切的大小差异取决于您的 App 使用的系统框架的数量。

关于 App 瘦身更多的信息,可以查看 Xcode Help 中的 What is app thinning? 有关应用程序文件大小的信息,请参考 App Store Connect Help 中的 View builds and file sizes

Swift 语言

新特性

标准Result类型

(SE-0235) Result在标准库中引入了一种类型,为我们提供了一种更简单,更清晰的方法来处理复杂代码中的错误,例如异步 API。

Swift 的Result类型实现为枚举,有两种情况:successfailure。两者都使用泛型实现,因此它们可以具有您选择的相关值,但failure必须符合 Swift 的Error类型。

为了演示Result,我们可以编写一个连接到服务器的函数来确定有多少未读消息正在等待用户。在此示例代码中,我们将只有一个可能的错误,即请求的 URL 字符串不是有效的 URL:

1
2
3
enum NetworkError: Error {
case badURL
}

提取函数将接受 URL 字符串作为其第一个参数,并将完成处理程序作为其第二个参数。该完成处理程序本身将接受一个 Result,其中成功案例将存储整数,并且失败案例会是某种NetworkError。我们实际上并没有在这里连接到服务器,但使用完成处理程序至少可以让我们模拟异步代码。

这是代码:

1
2
3
4
5
6
7
8
9
10
11
12
import Foundation
func fetchUnreadCount1(from urlString: String, completionHandler: @escaping (Result<Int, NetworkError>) -> Void) {
guard let url = URL(string: urlString) else {
completionHandler(.failure(.badURL))
return
}
// complicated networking code here
print("Fetching \(url.absoluteString)...")
completionHandler(.success(5))
}

要使用该代码,我们需要检查我们内部的值,Result以查看我们的调用是成功还是失败,如下所示:

1
2
3
4
5
6
7
8
fetchUnreadCount1(from: "https://www.hackingwithswift.com") { result in
switch result {
case .success(let count):
print("\(count) unread messages.")
case .failure(let error):
print(error.localizedDescription)
}
}

在你开始在自己的代码里面使用Result之前,这里还有三件事情你需要知道。

  • 首先,Result有一个get()方法,如果存在成功值,则返回成功值,否则抛出其错误。这允许您转换Result为常规抛出调用,如下所示:
1
2
3
4
5
fetchUnreadCount1(from: "https://www.hackingwithswift.com") { result in
if let count = try? result.get() {
print("\(count) unread messages.")
}
}
  • 其次,Result有一个接受抛出闭包的初始值设定项:如果闭包返回一个成功的值,用于该success情况,否则抛出的错误将放入该failure情况。

例如:

1
let result = Result { try String(contentsOfFile: someFile) }
  • 最后,您可以使用通用Error协议,而不是使用您创建的特定错误枚举。事实上,Swift Evolution 提案称 “预计结果的大部分用途都将Swift.Error用作Error类型参数。”

所以,与其使用Result<Int, NetworkError>,不如使用Result<Int, Error>。虽然这意味着你失去了类型抛出的安全性,但你可以抛出各种不同的错误枚举 - 你更喜欢这取决于你的编码风格。

原始字符串

(SE-0200)增加了创建原始字符串的功能,其中反斜杠和引号被解释为那些文字符号,而不是转义字符或字符串终止符。这使得许多用例更容易,但特别是正则表达式将受益。

要使用原始字符串,请在字符串前放置一个或多个#符号,如下所示:

1
let rain = #"The "rain" in "Spain" falls mainly on the Spaniards."#

字符串开头和结尾的#符号成为字符串分隔符的一部分,因此 Swift 理解 “rain” 和 “Spain” 周围的独立引号应该被视为文字引号而不是结束字符串。

原始字符串也允许您使用反斜杠:

1
let keypaths = #"Swift keypaths such as \Person.name hold uninvoked references to properties."#

这将反斜杠视为字符串中的文字字符,而不是转义字符。这反过来意味着字符串插值的工作方式不同:

1
2
let answer = 42
let dontpanic = #"The answer to life, the universe, and everything is \#(answer)."#

注意我之前如何用\#(answer)进行字符串插值 - 常规\(answer)将被解释为字符串中的字符,因此当您希望字符串插值在原始字符串中发生时,您必须添加额外的字符串#

Swift 原始字符串的一个有趣特性是在开头和结尾使用哈希符号,因为在不太可能的情况下你可以使用多个哈希符号。这里很难提供一个很好的例子,因为它真的应该是非常罕见的,但请考虑这个字符串:My dog said “woof”#gooddog。因为哈希之前没有空格,所以 Swift 会看到"#并立即将其解释为字符串终止符。在这种情况下,我们需要将分隔符更改#"##",如下所示:

1
let str = ##"My dog said "woof"#gooddog"##

注意末尾的哈希数必须与开头哈希符号个数一致。

原始字符串与 Swift 的多行字符串系统完全兼容 - 只需用于#"""启动,然后"""#结束,如下所示:

1
2
3
4
5
let multiline = #"""
The answer to life,
the universe,
and everything is \#(answer).
"""#

没有大量反斜杠这种特性就能在正则表达式中是相当有用的。例如,编写一个简单的正则表达式来查找关键路径,例如\Person.name以前应该是这样:

1
let regex1 = "\\\\[A-Z]+[A-Za-z]+\\.[a-z]+"

感谢原始字符串,我们可以使用一半的反斜杠数量来编写相同的内容:

1
let regex2 = #"\\[A-Z]+[A-Za-z]+\.[a-z]+"#

我们仍然需要一些反斜杠,因为正则表达式也使用它们。

自定义字符串插值

(SE-0228)大大改进了 Swift 的字符串插值系统,使其更高效,更灵活,并且创造了以前不可能实现的全新功能。

在最基本的形式中,新的字符串插值系统让我们可以控制对象在字符串中的显示方式。Swift 具有有助于调试的结构的默认行为,因为它打印结构名称后跟其所有属性。但是如果您正在使用类(没有这种行为),或者想要格式化该输出以使其面向用户,那么您可以使用新的字符串插值系统。

例如,如果我们有这样的结构:

1
2
3
4
struct User {
var name: String
var age: Int
}

如果我们想为它添加一个特殊的字符串插值,以便我们整齐地打印出用户,我们将String.StringInterpolation使用新的appendInterpolation()方法添加扩展。Swift 已经内置了数个实现方法,并且使用插值类型 ——在这种情况下 User 要确定调用哪个方法。

在这种情况下,我们将添加一个实现,将用户的名称和年龄放在一个字符串中,然后调用其中一个内置 appendInterpolation() 方法将其添加到我们的字符串中,如下所示:

1
2
3
4
5
extension String.StringInterpolation {
mutating func appendInterpolation(_ value: User) {
appendInterpolation("My name is \(value.name) and I'm \(value.age)")
}
}

现在我们可以创建一个用户并打印出他们的数据:

1
2
let user = User(name: "Guybrush Threepwood", age: 33)
print("User details: \(user)")

这将会打印 User details: My name is Guybrush Threepwood and I’m 33,而使用自定义字符串插值,它将打印User details: User(name: “Guybrush Threepwood”, age: 33)当然,该功能与仅实施CustomStringConvertible协议没有什么不同,所以让我们继续使用更高级的用法。

您的自定义插值方法可以根据需要使用任意数量的参数,标记或未标记。例如,我们可以使用各种样式添加插值来打印数字,如下所示:

1
2
3
4
5
6
7
8
9
10
extension String.StringInterpolation {
mutating func appendInterpolation(_ number: Int, style: NumberFormatter.Style) {
let formatter = NumberFormatter()
formatter.numberStyle = style
if let result = formatter.string(from: number as NSNumber) {
appendLiteral(result)
}
}
}

NumberFormatter类有多种style,包括货币($ 72.83),序数(1st,12th)和拼写(five, forty-three)。因此,我们可以创建一个随机数,并将其拼写成如下字符串:

1
2
3
let number = Int.random(in: 0...100)
let lucky = "The lucky number this week is \(number, style: .spellOut)."
print(lucky)

您可以根据需要多次调用appendLiteral(),如果有必要,甚至可以不调用。例如,我们可以添加一个字符串插值来多次重复一个字符串,如下所示:

1
2
3
4
5
6
7
8
9
extension String.StringInterpolation {
mutating func appendInterpolation(repeat str: String, _ count: Int) {
for _ in 0 ..< count {
appendLiteral(str)
}
}
}
print("Baby shark \(repeat: "doo ", 6)")

而且这些只是常规方法,您可以使用 Swift 的全部功能。例如,我们可能会添加一个将字符串数组连接在一起的插值,但如果该数组为空,则执行一个返回字符串的闭包:

1
2
3
4
5
6
7
8
9
10
11
12
extension String.StringInterpolation {
mutating func appendInterpolation(_ values: [String], empty defaultValue: @autoclosure () -> String) {
if values.count == 0 {
appendLiteral(defaultValue())
} else {
appendLiteral(values.joined(separator: ", "))
}
}
}
let names = ["Harry", "Ron", "Hermione"]
print("List of students: \(names, empty: "No one").")

使用@autoclosure意味着我们可以使用简单值或调用复杂函数作为默认值,但除非values.count为零,否则不会完成任何工作。

通过ExpressibleByStringLiteralExpressibleByStringInterpolation协议的组合,现在可以使用字符串插值创建整个类型,如果我们添加CustomStringConvertible, 我们想要的话,甚至可以将这些类型打印为字符串。

为了实现这个功能,我们需要满足一些特定的标准:

  • 无论我们创造什么类型都应该符合ExpressibleByStringLiteralExpressibleByStringInterpolationCustomStringConvertible。只有在您想要自定义打印类型的方式时才需要后者。
  • 你的类型内部必须是一个StringInterpolation且符合的嵌套结构StringInterpolationProtocol
  • 嵌套的 struct 需要有一个初始化器,它接受两个整数,告诉我们大概可以预期的数据量。
  • 它还需要实现一种appendLiteral()方法,以及一种或多种appendInterpolation()方法。
  • 您的主类型需要有两个初始化程序,允许从字符串文字和字符串插值创建它。

我们可以将所有这些放在一个可以从各种常见元素构造 HTML 的示例类型中。嵌套StringInterpolation结构中的 “暂存器” 将是一个字符串:每次添加新的文字或插值时,我们都会将其附加到字符串中。为了帮助您确切了解发生了什么,我在各种追加方法中添加了一些print()调用。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
struct HTMLComponent: ExpressibleByStringLiteral, ExpressibleByStringInterpolation, CustomStringConvertible {
struct StringInterpolation: StringInterpolationProtocol {
// start with an empty string
var output = ""
// allocate enough space to hold twice the amount of literal text
init(literalCapacity: Int, interpolationCount: Int) {
output.reserveCapacity(literalCapacity * 2)
}
// a hard-coded piece of text – just add it
mutating func appendLiteral(_ literal: String) {
print("Appending \(literal)")
output.append(literal)
}
// a Twitter username – add it as a link
mutating func appendInterpolation(twitter: String) {
print("Appending \(twitter)")
output.append("<a href=\"https://twitter/\(twitter)\">@\(twitter)</a>")
}
// an email address – add it using mailto
mutating func appendInterpolation(email: String) {
print("Appending \(email)")
output.append("<a href=\"mailto:\(email)\">\(email)</a>")
}
}
// the finished text for this whole component
let description: String
// create an instance from a literal string
init(stringLiteral value: String) {
description = value
}
// create an instance from an interpolated string
init(stringInterpolation: StringInterpolation) {
description = stringInterpolation.output
}
}

我们现在可以创建和使用HTMLComponent字符串插值的实例,如下所示:

1
2
let text: HTMLComponent = "You should follow me on Twitter \(twitter: "twostraws"), or you can email me at \(email: "[email protected]")."
print(text)

多亏了print()分散在内部的调用,你会看到字符串插值功能的确切工作原理:你会看到 “Appending You should follow me on Twitter”,“Appending twostraws”,“Appending , or you can email me at ” ,“Appending [email protected]”,最后 “Appending .” ——每个部分触发一个方法调用,并添加到我们的字符串中。

动态可调用类型

(SE-0216) @dynamicCallable 允许您使用一个简单的语法糖像调用函数一样来调用命名类型。主要的应用场景是动态语言互操作。

有效的将这段代码:

1
let result = random(numberOfZeroes: 3)

转化成:

1
let result = random.dynamicallyCall(withKeywordArguments: ["numberOfZeroes": 3])

之前Paul Hudson 写过 feature in Swift 4.2 called @dynamicMemberLookup@dynamicCallable@dynamicMemberLookup 的自然扩展,并且服务于同一目的:使 Swift 代码更容易与 Python 和 JavaScript 等动态语言一起工作。

要将此功能添加到您自己的类型,您需要添加@dynamicCallable属性以及这些方法中的一个或两个:

1
2
3
func dynamicallyCall(withArguments args: [Int]) -> Double
func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> Double

第一个是当你调用不带参数的标识类型(例如,其中第一个使用a(b, c))时使用的,第二个是当你确定提供标识的时候用的(如a(b: cat, c: dog))。

@dynamicCallable 对于那些能够接受和返回的数据类型的方法来说非常灵活,使您可以从 Swift 的所有类型安全性中受益,同时仍然具有一些用于高级用途的蠕动空间(wriggle room)。因此,对于第一种方法(无参数标识),您可以使用任意符合ExpressibleByArrayLiteral的东西,诸如数组,数组切片和集合的任何内容,对于第二种方法(使用参数标识),您可以使用任何符合ExpressibleByDictionaryLiteral 诸如字典和键值对的任何内容。

  • 注意:如果您之前没有使用过 KeyValuePairs,那么现在是了解它们的最佳时机,因为它们和@dynamicCallable 一起使用非常有用。在这里了解更多:KeyValuePairs 是什么?

除了接受各种输入外,您还可以为各种输出提供多个重载 —— 一个可能返回一个字符串,一个是整数,依此类推。Swift 能够解决什么就使用使用哪一个,你就可以混合搭配你想要的一切。

我们来看一个例子。首先,这是一个叫做 RandomNumberGenerator 的结构,生成介于 0 和某个最大值之间的数字,具体取决于传入的输入:

1
2
3
4
5
6
struct RandomNumberGenerator {
func generate(numberOfZeroes: Int) -> Double {
let maximum = pow(10, Double(numberOfZeroes))
return Double.random(in: 0...maximum)
}
}

把它切换到@dynamicCallable我们需要这样写:

1
2
3
4
5
6
7
8
@dynamicCallable
struct RandomNumberGenerator {
func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> Double {
let numberOfZeroes = Double(args.first?.value ?? 0)
let maximum = pow(10, numberOfZeroes)
return Double.random(in: 0...maximum)
}
}

可以使用任意数量的参数调用该方法,或者可能为零,因此我们仔细读取第一个值并使用 nil 合并以确保存在可能出现的默认值。

我们现在可以创建一个实例RandomNumberGenerator 并将其声明为函数:

1
2
let random = RandomNumberGenerator()
let result = random(numberOfZeroes: 0)

如果您曾经使用过dynamicallyCall(withArguments:)- 或者同时使用它们,因为您可以将它们都用于单一类型 - 那么您可以这样写:

1
2
3
4
5
6
7
8
9
10
11
@dynamicCallable
struct RandomNumberGenerator {
func dynamicallyCall(withArguments args: [Int]) -> Double {
let numberOfZeroes = Double(args[0])
let maximum = pow(10, numberOfZeroes)
return Double.random(in: 0...maximum)
}
}
let random = RandomNumberGenerator()
let result = random(0)

使用@dynamicCallable时需要注意一些重要的规则:

  • 您可以将它应用于结构,枚举,类和协议。
  • 如果你实现withKeywordArguments:并且没有实现withArguments:,你的类型仍然可以在没有参数标签的情况下调用 - 你将会只能获取键的空字符串。
  • 如果您的实现withKeywordArguments:withArguments:被标记为 throw,则调用该类型也将被抛出。
  • 您无法添加@dynamicCallable到一个扩展,只能添加类型的主要定义。
  • 您仍然可以为您的类型添加其他方法和属性,并正常使用它们。

也许更重要的是,此不支持方法解析,这意味着我们必须直接调用类型(例如random(numberOfZeroes: 5))而不是调用类型上的特定方法(例如random.generate(numberOfZeroes: 5))。已经有一些关于使用方法签名添加后者的讨论,例如:

1
func dynamicallyCallMethod(named: String, withKeywordArguments: KeyValuePairs<String, Int>)

如果在未来的 Swift 版本中成为可能,它可能会为测试模拟开辟一些非常有趣的可能性。

与此同时,@dynamicCallable 不太可能广受欢迎,但对于少数想要与 Python,JavaScript 和其他语言交互的人来说,这一点非常重要。

这里还有一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
@dynamicCallable struct ToyCallable {
func dynamicCall(withArguments:[Int]){}
func dynamicCallwithKeywordArgumentsKeyValuePairs <String,Int>){}
}
let x = ToyCallable()
x(1,2,3
// Equals to `x.dynamicallyCall(withArguments:[1,2,3])`
x(label: 1, 2)
// Equals to `x.dynamicallyCall(withKeywordArguments: ["label": 1, "": 2])`

处理未来的枚举

(SE-0192)增加了区分固定枚举和未来可能发生变化的枚举的功能。

Swift 的一个安全功能是它要求所有 switch 语句都是详尽无遗的 - 它们必须覆盖所有情况。虽然这从安全角度来看效果很好,但是在将来添加新案例时会导致兼容性问题:系统框架可能会发送您未提供的不同内容,或者您依赖的代码可能会添加新案例并导致您的编译中断,因为你的switch不再详尽无遗。

通过该@unknown属性,我们现在可以区分两个略有不同的场景:“这个默认情况应该针对所有其他情况运行,因为我不想单独处理它们” 和 “我想单独处理所有情况,但如果将来出现的话而不是导致错误。“

这是一个 enum 示例:

1
2
3
4
5
enum PasswordError: Error {
case short
case obvious
case simple
}

我们可以使用switch块编写代码来处理每个案例:

1
2
3
4
5
6
7
8
9
10
func showOld(error: PasswordError) {
switch error {
case .short:
print("Your password was too short.")
case .obvious:
print("Your password was too obvious.")
default:
print("Your password was too simple.")
}
}

对于短密码和明显密码,它使用两个显式情况,但将第三种情况捆绑到默认块中。

现在,如果将来我们在 enum 中添加了一个新的 case old,用于判断是否为之前使用过的密码,我们的default情况会被自动调用,即使它的消息没有道理 - 密码也许并不简单。

Swift 无法向我们发出有关此代码的警告,因为它在技术上是正确的,因此很容易错过这个错误。幸运的是,新d的 @unknown 属性完美地修复了它——它只能在default案例中使用,并且设计为在将来出现新案例时运行。

例如:

1
2
3
4
5
6
7
8
9
10
func showNew(error: PasswordError) {
switch error {
case .short:
print("Your password was too short.")
case .obvious:
print("Your password was too obvious.")
@unknown default:
print("Your password wasn't suitable.")
}
}

该代码现在将发出警告,因为该switch块不再详尽无遗 —— Swift 希望我们明确处理每个案例。实际上这只是一个警告,这使得这个属性如此有用:如果一个框架在未来添加一个新案例,你会收到警告,但它不会破坏你的源代码。

不仅如此,在 Swift 5 之前,您可以编写一个带有可变参数的枚举 case:

1
2
3
4
5
6
enum X {
case foo(bar: Int...)
}
func baz() -> X {
return .foo(bar: 0, 1, 2, 3)
}

之前不是特意要支持这个特性,而且现在这样写会报错了。

取而代之的是,让枚举的 case 携带一个数组,并显式传递一个数组:

1
2
3
4
5
6
7
enum X {
case foo(bar: [Int])
}
func baz() -> X {
return .foo(bar: [0, 1, 2, 3])
}

展平嵌套try?的可选结果

(SE-0230)修改了工作方式,try?使嵌套的选项变平,成为常规选项。这使得它的工作方式与可选链接和条件类型转换的工作方式相同,这两种方法都在早期的 Swift 版本中展平了选项。

这是一个演示变化的实际示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct User {
var id: Int
init?(id: Int) {
if id < 1 {
return nil
}
self.id = id
}
func getMessages() throws -> String {
// complicated code here
return "No messages"
}
}
let user = User(id: 1)
let messages = try? user?.getMessages()

User结构具有可失败的初始化程序,因为我们希望确保用户创建具有有效 ID 的用户。getMessages()方法理论上应该包含某种复杂的代码,以获取用户的所有消息列表,因此标记为throws; 我已经让它返回一个固定的字符串,所以代码可以编译。

关键是最后一行:因为用户是可选的,它使用可选的链接,因为getMessages()可以抛出它用于try?将 throw 方法转换为可选的,所以我们最终得到一个嵌套的可选。在 Swift 4.2 和更早版本中,这将让 messagesString??类型 —— 一个可选的可选字符串 —— 但是在 Swift 5.0 及更高版本的try?中,如果它们已经是可选的,它们将不会将值包装在可选中,因此它messages只是一个String?

此新行为与可选链接和条件类型转换的现有行为相匹配。也就是说,如果需要,可以在一行代码中使用可选链接十几次,但最终不会有 12 个嵌套选项。同样,如果你使用了可选的链接as?,你仍然只有一个级别的可选性,因为这通常是你想要的结果。

检查整数倍数

(SE-0225)为整数添加了一个 isMultiple(of:)方法,允许我们使用更清晰的方式检查一个数是否是另一个数的倍数%,而不是使用除法余数。

例如:

1
2
3
4
5
6
7
let rowNumber = 4
if rowNumber.isMultiple(of: 2) {
print("Even")
} else {
print("Odd")
}

是的,我们可以用if rowNumber % 2 == 0完成同样的检查功能,但你必须承认这个方法不太清楚 - isMultiple(of:)作为方法意味着它可以在 Xcode 的代码完成选项中列出,这有助于理解代码和发现问题

使用 compactMapValues()转换和解包字典值

(SE-0218) 为字典添加了一种新方法compactMapValues(),将来自数组的功能compactMap()(“转换我的值,解包结果,然后丢弃任何无效的东西”)与字典中的mapValues()方法结合在一起(“保持我的密钥完整但转换我的值”) 。

作为一个例子,这里是一个人们在比赛中的字典,包含了他们在几秒钟内完成的时间。一个人没有完成,标记为 “DNF” (DO NOT FINISHED):

1
2
3
4
5
6
let times = [
"Hudson": "38",
"Clarke": "42",
"Robinson": "35",
"Hartis": "DNF"
]

我们可以使用compactMapValues()创建一个名称和时间为整数的新字典,删除一个 DNF 人员:

1
let finishers1 = times.compactMapValues { Int($0) }

或者,您可以直接将Int初始化程序传递给compactMapValues(),如下所示:

1
let finishers2 = times.compactMapValues(Int.init)

您还可以使用compactMapValues()解包选项并丢弃 nil 值而不执行任何类型的转换,如下所示:

1
2
3
4
5
6
7
8
let people = [
"Paul": 38,
"Sophie": 8,
"Charlotte": 5,
"William": nil
]
let knownAges = people.compactMapValues { $0 }

计算序列中的匹配项

这个 Swift 5.0 新增的功能在 beta 测试中被撤销,因为它导致了类型检查器的性能问题。希望它能够及时回到 Swift 5.1,或者用一个新名称来避免问题。

(SE-0220)引入了一种新的count(where:)方法,该方法和filiter()有相同的表现,而且包含单个的值传递。此举免去了去创建一个一分配就要马上被释放掉的数组,并为常见问题提供清晰简洁的解决方案。

此示例创建一个测试结果数组,并计算大于或等于 85 的数量:

1
2
let scores = [100, 80, 85]
let passCount = scores.count { $0 >= 85 }

这计算了数组中以 “Terry” 开头的名称数量:

1
2
let pythons = ["Eric Idle", "Graham Chapman", "John Cleese", "Michael Palin", "Terry Gilliam", "Terry Jones"]
let terryCount = pythons.count { $0.hasPrefix("Terry") }

此方法适用于所有符合的类型Sequence,因此您也可以在集合和字典上使用它。

其它新特性(部分)

  • Key path 现在支持特性 (identity) keypath (\.self),这是一个引用自身完整输入值的 WritableKeyPath。(SE-0227)
1
2
3
4
5
let id = \Int.self
var x = 2
print(x[keyPath: id]) // Prints "2"
x[keyPath: id] = 3
print(x[keyPath: id]) // Prints "3"
  • 如果类型 T 符合 Initialized with Literals 中的其中一个协议(如 ExpressibleByIntegerLiteral),且 literal 是一个字面量表达示时,则 T(literal) 会使用相应的协议创建一个类型 T 的字面量,而不是使用一个协议的默认字面量类型的值来调用 Tinitializer

    如,类似于 UInt64(0xffff_ffff_ffff_ffff) 这样的表达式现在是有效的,则之前会由于整型字面量的默认类型是 Int,而导致溢出。(SE-0213)

  • 提高了字符串插值操作的性能、清晰性和效率。(SE-0228)

    旧的 _ExpressibleByStringInterpolation 协议被删除;如果您有使用此协议的代码,则需要做相应更新。您可以使用 #if 条件判断来区分 Swift 4.2Swift 5 的代码。例如:

1
2
3
4
5
#if compiler(<5)
extension MyType: _ExpressibleByStringInterpolation { /*...*/ }
#else
extension MyType: ExpressibleByStringInterpolation { /*...*/ }
#endif

Swift 标准库

新功能

  • DictionaryLiteral 类型重命名为 KeyValuePairs。(SE-0214)

  • 桥接到 Objective-C 代码的 Swift 字符串现在可以在适当的时候从 CFStringGetCStringPtr 返回一个 non-nil 值,同时从 -UTF8String 返回的指针与字符串的生命周期相关联,而不是最相近的那个 autorelease pool。如果程序正确,那应该没有任何问题,并且会发现性能显著提高。但是,这也可能会让之前一些未经测试的代码运行,从而暴露一些潜在的问题;例如,如果有一个对 non-nil 值的判断,而相应分支在 Swift 5 之前却从未被执行过。(26236614)

  • Sequence 协议不再具有 SubSequence 关联类型。先前返回 SubSequenceSequence 方法现在会返回具体类型。例如,suffix(_:) 现在会返回一个 Array。(47323459)

    使用 SubSequenceSequence 扩展应该修改为类似地使用具体类型,或者修改为 Collection 的扩展,在 CollectionSubSequence 仍然可用。(45761817)

例如:

1
2
3
4
5
extension Sequence {
func dropTwo() -> SubSequence {
return self.dropFirst(2)
}
}

需要改为:

1
2
3
4
5
extension Sequence {
func dropTwo() -> DropFirstSequence<Self> {
return self.dropFirst(2)
}
}

或者是:

1
2
3
4
5
extension Collection {
func dropTwo() -> SubSequence {
return self.dropFirst(2)
}
}
  • String 结构的原生编码将从 UTF-16 切换到 UTF-8,与 String.UTF16View 相比,这会提高相关联的 String.UTF8View 的性能。重新对所有代码进行评审以提高性能,尤其是使用了 String.UTF16View 的代码。

Swift 包管理器

新功能

  • 现在,在使用 Swift 5 软件包管理器时,Targets 可以声明一些常用的针对特定目标的 build settings 设置。新设置也可以基于平台和构建配置进行条件化处理。包含的构建设置支持 SwiftC 语言定义,C 语言头文件搜索路径,链接库和链接框架。(SE-0238)(23270646)

  • 在使用 Swift 5 软件包管理器时,package 现在可以自定义 Apple 平台的最低 deployment target。而如果 package A 依赖于 package B,但 package B 指定的最小 deployment target 高于 package A 的最小 deployment target,则构建 package A 时会抛出错误。(SE-0236)(28253354)

  • 新的依赖镜像功能允许顶层包覆盖依赖 URL。(SE-0219)(42511642)

    使用以下命令设置镜像:

1
2
$ swift package config set-mirror \
--package-url <original URL> --mirror-url <mirror URL>
  • swift 测试命令可以使用标志 --enable-code-coverage,来生成标准格式的代码覆盖率数据,以便其它代码覆盖工具使用。生成的代码覆盖率数据存储在 <build-dir>/<configuration>/codecov 目录中。
  • Swift 5 不再支持 Swift 3 版本的软件包管理器。仍然在使用 Swift 3 Package.swift 工具版本(tool-version)上的软件包应该更新到新的工具版本上。
  • 对体积较大的包进行包管理器操作现在明显更快了。
  • Swift 包管理器有一个新的 --disable-automatic-resolution 标志项,当 Package.resolved 条目不再与 Package.swift 清单文件中指定的依赖项版本兼容时,该标志项强制包解析失败。此功能对于持续集成系统非常有用,可以检查包的 Package.resolved 是否已过期。
  • swift run 命令有一个新的 --repl 选项,它会启动 Swift REPL,支持导入包的库目标。这使您可以轻松地从包目标中试用 API,而无需构建调用该 API 的可执行文件。
  • 有关使用 Swift 包管理器的更多信息,请访问 swift.org 上的 Using the Package Manager

Swift 编译器

新特性

  • 现在,在优化(-O-Osize)构建中,默认情况下在运行时强制执行独占内存访问。违反排他性的程序将在运行时抛出带有 “重叠访问” 诊断消息错误。您可以使用命令行标志禁用此命令:-enforce-exclusivity = unchecked,但这样做可能会导致未定义的行为。运行时违反排他性通常是由于同时访问类属性,全局变量(包括顶层代码中的变量)或通过 eacaping 闭包捕获的变量。(SR-7139

  • Swift 3 运行模式已被删除。-swift-version 标志支持的值为 44.25

  • 在 Swift 5 中,在 switch 语句中使用 Objective-C 中声明的或来自系统框架的枚举时,必须处理未知的 case,这些 case 可能将来会添加,也可能是在 Objective-C 实现文件中私下定义。形式上,Objective-C 允许在枚举中存储任何值,只要它匹配底层类型即可。这些未知的 case 可以使用新的 @unknown default case 来处理,当然如果 switch 中省略了任何已知的 case,编译器仍然会给出警告。它们也可以使用普通的 default case 来处理。

    如果您已在 Objective-C 中定义了自己的枚举,并且不需要客户端来处理 unknown case,则可以使用 NS_CLOSED_ENUM 宏而不是 NS_ENUM。Swift 编译器识别出这一点,并且不需求 switch 语句必须带有 default case

    Swift 44.2 模式下,您仍然可以使用 @unknown default。如果省略 @unknown default,而又传递了一个未知的值,则程序在运行时抛出异常,这与 Xcode 10.1 中的 Swift 4.2 上的行为是一致的。(SE-0192)(39367045)

  • 现在在 SourceKit 生成的 Swift 模块接口中会打印默认参数,而不仅仅是使用占位符。

  • unownedunowned(unsafe) 类型的变量现在支持可选类型。

已知的问题

  • 如果引用了 UIAccessibility 结构的任何成员,则 Swift 编译器会在 “Merge swiftmodule” 构建步骤中崩溃。构建日志包含一条消息:
1
2
3
4
Cross-reference to module 'UIKit'
... UIAccessibility
... in an extension in module 'UIKit'
... GuidedAccessError

包含 NS_ERROR_ENUM 枚举的其他类型也可能出现此问题,但 UIAccessibility 是最常见的。(47152185)

解决方法:在 targetBuild Setting -> Swift Compiler -> Code Generation 下,设置 Compilation Mode 的值为 Whole Module。这是大多数 Release 配置的默认设置。

  • 为了减小 Swift 元数据的大小,Swift 中定义的 convenience initializers 如果调用了 Objective-C 中定义的一个 designated initializer,那只会提前分配一个对象。在大多数情况下,这对您的程序没有影响,但如果从 Objective-C 调用 convenience initializers,那么 +alloc 分配的初始内存会被释放,而不会调用任何 initializer。对于不希望发生任何类型的对象替换的调用者来说,这可能是有问题的。其中一个例子是 initWithCoder: :如果 NSKeyedUnarchiver 调用 Swift 实现 init(coder:) 并且存档对象存在循环时,则 NSKeyedUnarchiver 的实现可能会出错。

    在将来的版本中,编译器将保证一个 convenience initializer 永远不会丢弃它所调用的对象,只要它通过 self.init 委托给它的初始化程序也暴露给 Objective-C,或者是它在 Objective-C 中定义了,或者是使用 @objc 标记的,或者是重写了一个暴露给 Objective-Cinitializer,或者是它满足 @objc 协议的要求。(46823518)

  • 如果一个 keypath 字面量引用了 Objective-C 中定义的属性,或者是在 Swift 中使用 @objcdynamic 修饰符定义的属性,则编译可能会失败,并且报 “unsupported relocation of local symbol 'L_selector'” 错误,或者 key path 字面量无法在运行时生成正确的哈希值或处理相等比较。

    解决方法:您可以定义一个不是 @objc 修饰的包装属性,来引用这个 key path。得到的 key path 与引用原始 Objective-C 属性的 key path 不相等,但使用包装属性效果是相同的。

  • 某些项目可能会遇到以前版本的编译时回归。

  • Swift 命令行项目在启动时因抛出 “dyld:Library not loaded” 错误而崩溃。

    解决方法:添加自定义的构建设置 SWIFT_FORCE_STATIC_LINK_STDLIB=YES

已解决的问题

  • 扩展绑定现在支持嵌套类型的扩展,这些嵌套类型本身是在扩展内定义的。之前可能会因为一些声明顺序而失败,并产生 “未声明类型” 错误。(SR-631)

  • 在 Swift 5 中,返回 Self 的类方法不能再被使用返回非 final 的具体类类型的方法来覆盖。此类代码不是类型安全的,需要更新。(SR-695)

    例如:

1
2
3
4
5
6
7
class Base {
class func factory() -> Self { /*...*/ }
}
class Derived: Base {
class override func factory() -> Derived { /*...*/ }
}
  • 在 Swift 5 模式下,现在会明确禁止声明与嵌套类型同名的静态属性。以前,可以在泛型类型的扩展中执行这样的声明。(SR-7251)

    例如:

1
2
3
4
5
6
7
8
9
struct Foo<T> {}
extension Foo {
struct i {}
// Error: Invalid redeclaration of 'i'.
// (Prior to Swift 5, this didn’t produce an error.)
static var i: Int { return 0 }
}
  • 现在可以在子类里继承父类中具有可变参数的初始化方法。

  • 在 Swift 5 中,函数中的 @autoclosure 参数不能再作为 @autoclosure 参数传递到另一个函数中调用。相反,您必须使用括号显式调用函数值:();调用本身包含在一个隐式闭包中,保证了与 Swift 4 相同的行为。(SR-5719

    例如:

1
2
3
4
5
func foo(_ fn: @autoclosure () -> Int) {}
func bar(_ fn: @autoclosure () -> Int) {
foo(fn) // Incorrect, `fn` can’t be forwarded and has to be called.
foo(fn()) // OK
}
  • 现在完全支持在类和泛型中定义复杂的递归类型,而此前可能会导致死锁。

  • 在 Swift 5 中,当将可选值转换为泛型占位符类型时,编译器在解包值时会更加谨慎。这种转换的结果现在更接近于非泛型上下文中的结果。(SR-4248)

    例如:

1
2
3
4
5
6
7
8
9
10
11
func forceCast<U>(_ value: Any?, to type: U.Type) -> U {
return value as! U
}
let value: Any? = 42
print(forceCast(value, to: Any.self))
// Prints "Optional(42)"
// (Prior to Swift 5, this would print "42".)
print(value as! Any)
// Prints "Optional(42)"
  • 协议现在可以将它们的实现类型限定为指定类型的子类。支持两种等效形式:
1
2
protocol MyView: UIView { /*...*/ }
protocol MyView where Self: UIView { /*...*/ }

Swift 4.2 接受了第二种形式,但没有完全实现,有时可能在编译时或运行时崩溃。(SR-5581)

  • 在 Swift 5 中,当在属性自身的 didSetwillSet 中设置属性本身时,会避免递归调用(不论是隐式或显式地设置自身的属性)。(SR-419)

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Node {
var children = [Node]()
var depth: Int = 0 {
didSet {
if depth < 0 {
// Won’t recursively call didSet, because this is setting depth on self.
depth = 0
}
// Will call didSet for each of the children,
// as this isn’t setting the property on self.
// (Prior to Swift 5, this didn’t trigger property
// observers to be called again.)
for child in children {
child.depth = depth + 1
}
}
}
}
  • Xcode 中 的 diagnostics#sourceLocation 进行了支持。也就是说,如果您使用#sourceLocation 将生成的文件中的行映射回源代码时,diagnostics 会显示在原始源文件中的行数而不是生成的文件中的。
  • 使用泛型类型别名作为参数或 @objc 方法的返回类型,不再导致生成无效的 Objective-C header。(SR-8697)

引用

知识小集

What’s new in Swift 5.0

Title: Swift 5 做了哪些改动?(最低支持 Xcode 10.2 beta)/ 3.24 updated

Author: Tuski

Published: 03/24/2019 - 22:02:34

Updated: 03/26/2019 - 16:37:50

Link: http://www.perphet.com/2019/03/Swift-5-0-做了哪些改动?-3-24-Updated/

Protocol: Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0) Reprinted please keep the original link and author

Thx F Sup