Swift 5.5 新特性抢先看,async/await 将重磅来袭

知识小集

共 15909字,需浏览 32分钟

 · 2021-06-01

再过一周的时间,WWDC21 就正式举行了,如果不出意外的话,Swift 5.5 测试版也会在期间发布。早在 3 月 13 日,官方论坛就公布了 Swift 5.5 版本的发布计划,并在 4 月 16 日拉出了 release/5.5 分支。经过几个月时间的准备,从 Swift Evolution 中,我们能发现 Swift 5.5 将为我们带来许多期待已久的特性,如 async/await。我们今天就来简单整理一下这些 可能 将在 Swift 5.5 中出现的新特性。

SE-0291 包集合

这个 proposal 旨在为 SwiftPM 添加对包集合的支持。包集合是包和相关元数据的列表,可以更轻松地发现特定用例的现有包。SwiftPM 将允许用户订阅这些集合,通过 swift package-collection 命令行界面搜索它们,并使 libSwiftPM 的任何客户端都可以访问它们的内容。这个 proposal 关注于以命令行界面和包集合想着的配置数据格式。

例如,list 命令将列出用户配置的所有集合:

$ swift package-collection list [--json]
My organisation's packages - https://example.com/packages.json
...

describe 命令显示来自包本身的元数据。

$ swift package-collection describe [--json] https://github.com/jpsim/yams
Description: A sweet and swifty YAML parser built on LibYAML.
Available Versions: 4.0.0, 3.0.0, ...
Watchers: 14
Readme: https://github.com/jpsim/Yams/blob/master/README.md
Authors: @norio-nomura, @jpsim
--------------------------------------------------------------
Latest Version: 4.0.0
Package Name: Yams
Modules: Yams, CYaml
Supported Platforms: iOS, macOS, Linux, tvOS, watchOS
Supported Swift Versions: 5.3, 5.2, 5.1, 5.0
License: MIT
CVEs: ...

https://github.com/apple/swift-evolution/blob/main/proposals/0291-package-collections.md

SE-0293 将属性包装器扩展到函数和闭包参数

Property Wrappers 用于抽象出常见的属性访问器模式,在 Swift 5.1 中引入。不过之前仅允许在局部变量和类型属性上应用属性包装器。而这个 proposal 的目标是将属性包装器扩展到函数和闭包参数。

例如,使用来自 PropertyKit 的验证,我们可以将各种前提条件抽象到一个属性包装器中:

@propertyWrapper
struct Asserted<Value> {
init(
wrappedValue: Value,
validation: Validation<Value>,
) { ... }

var wrappedValue: Value { ... }
}

将 @Asserted 应用于参数以对参数值断言某些先决条件会很有用。例如,下面的代码断言传递给 quantity 参数的参数大于或等于1:

func buy(
@Asserted(.greaterOrEqual(1)) quantity: Int,
of product: Product,
) { ... }

https://github.com/apple/swift-evolution/blob/main/proposals/0293-extend-property-wrappers-to-function-and-closure-parameters.md

SE-0295 有关联值的枚举的 Codable 合成

在 SE-0166 中引入了 Codable,它支持合成类和结构类型的 Encodable 和 Decodable 一致性,其中仅包含也符合各自协议的值。

这个 proposal 将扩展对枚举关联值的一致性的自动合成的支持。

如以下枚举有关联值:

enum Command: Codable {
case load(key: String)
case store(key: String, value: Int)
}

将会被编码为

{
"load": {
"key": "MyKey"
}
}

{
"store": {
"key": "MyKey",
"value": 42
}
}

编译器将生成以下 CodingKeys 声明:

// contains keys for all cases of the enum
enum CodingKeys: CodingKey {
case load
case store
}

// contains keys for all associated values of `case load`
enum LoadCodingKeys: CodingKey {
case key
}

// contains keys for all associated values of `case store`
enum StoreCodingKeys: CodingKey {
case key
case value
}

https://github.com/apple/swift-evolution/blob/main/proposals/0295-codable-synthesis-for-enums-with-associated-values.md

SE-0296 async/await

现代 Swift 开发涉及大量使用闭包和完成处理程序的异步编程,但这些 API 都很难使用。当使用许多异步操作、错误处理或异步调用之间的控制流变得复杂时,会让问题变得很复杂。我们先看一个简单的例子:

// 一系列简单的异步操作通常需要深度嵌套的闭包
func processImageData1(completionBlock: (_ result: Image) -> Void) {
loadWebResource("dataprofile.txt") { dataResource in
loadWebResource("imagedata.dat") { imageResource in
decodeImage(dataResource, imageResource) { imageTmp in
dewarpAndCleanupImage(imageTmp) { imageResult in
completionBlock(imageResult)
}
}
}
}
}

processImageData1 { image in
display(image)
}

而这个提案则引入了一种语言扩展,大大简化了这些操作,让代码更加自然而不易出错。这种设计为 Swift 引入了一个协程模型。函数可以是 async,允许程序员使用正常的控制流机制编写涉及异步操作的复杂逻辑。编译器负责将异步函数转换为一组合适的闭包和状态机。实际上 async/await 语义早已是现代编程语言的标配。我们来看看,async/await 如何让代码变得更加简洁:

func loadWebResource(_ path: String) async throws -> Resource
func decodeImage(_ r1: Resource, _ r2: Resource) async throws -> Image
func dewarpAndCleanupImage(_ i : Image) async throws -> Image

func processImageData() async throws -> Image {
let dataResource = try await loadWebResource("dataprofile.txt")
let imageResource = try await loadWebResource("imagedata.dat")
let imageTmp = try await decodeImage(dataResource, imageResource)
let imageResult = try await dewarpAndCleanupImage(imageTmp)
return imageResult
}

不过,这个 proposal 并不提供并发,结构化并发问题由另一个 proposal 引入,它将异步函数与并发执行的任务相关联,并提供用于创建、查询和取消任务的 API。

https://github.com/apple/swift-evolution/blob/main/proposals/0296-async-await.md

SE-0297 与 Objective-C 的并发互操作性

在 Apple 平台上,Swift 与 Objective-C 的混编与交互目前来讲还是一个很大的课题。在 Objective-C 中,异步 API 随处可见,在 iOS 14.0 SDK 中就包含了近 1000 个接受 completion 处理器的方法。例如以下 PassKit API 中的方法

- (void)signData:(NSData *)signData 
withSecureElementPass:(PKSecureElementPass *)secureElementPass
completion:(void (^)(NSData *signedData, NSData *signature, NSError *error))completion;

这些方法包括可以直接在 Swift 中调用的方法,可以在 Swift 定义的子类中覆盖的方法,以及可以实现的协议中的方法。

当前,以上 Objective-C 函数在 Swift 中会被翻译成以下函数:

@objc func sign(_ signData: Data, 
using secureElementPass: PKSecureElementPass,
completion: @escaping (Data?, Data?, Error?) -> Void
)

这个 proposal 旨在为 Swift 并发结构与 Objective-C 之间提供互操作性,并实现以下目标:

  • 将 Objective-C 完成处理程序方法转换为 Swift 中的 async 方法;

  • 允许将 Swift 中定义的 async 方法标记为 @objc,在这种情况下,它们将作为完成处理程序方法导出;

  • 提供 Objective-C 属性来控制如何将基于完成处理程序的 API 转换为 asyncSwift 函数。

基于这些假设,以上 Objective-C 函数将会被翻译为以下 async 函数:

@objc func sign(
_ signData: Data,
using secureElementPass: PKSecureElementPass
)
async throws -> (Data, Data)

同时可以通过以下方式来调用:

let (signedValue, signature) = try await passLibrary.sign(signData, using: pass)

https://github.com/apple/swift-evolution/blob/main/proposals/0297-concurrency-objc.md

SE-0298 Async/Await: 序列

SE-0296 的 async/await 特性为 Swift 提供了更直观的异步编程方式。而这个 proposal 的目标是基于 async/await,以内置的方式更直观地编写和使用随时间返回多个值的函数。

这个 proposal 主要由三个部分组成:

  • 表示异步值序列的协议的标准库定义;

  • 编译器支持在异步值序列上使用 for...in 语法;

  • 对异步值序列进行操作的常用函数的标准库实现

基于这个 proposal,迭代异步值序列像迭代同步值序列一样简单。一个示例用例是迭代文件中的行,如下所示:

for try await line in myFile.lines() {
// Do something with each line
}

为此,标准库中定义了以下协议

public protocol AsyncSequence {
associatedtype AsyncIterator: AsyncIteratorProtocol where AsyncIterator.Element == Element
associatedtype Element
__consuming func makeAsyncIterator() -> AsyncIterator
}

public protocol AsyncIteratorProtocol {
associatedtype Element
mutating func next() async throws -> Element?
}

编译器生成的代码将允许在符合 AsyncSequence 的任何类型上使用for in循环。标准库还将扩展协议以提供熟悉的通用算法。如以下示例:

struct Counter : AsyncSequence {
let howHigh: Int

struct AsyncIterator : AsyncIteratorProtocol {
let howHigh: Int
var current = 1
mutating func next() async -> Int? {
// We could use the `Task` API to check for cancellation here and return early.
guard current <= howHigh else {
return nil
}

let result = current
current += 1
return result
}
}

func makeAsyncIterator() -> AsyncIterator {
return AsyncIterator(howHigh: howHigh)
}
}

在调用端,则可以如下使用:

for await i in Counter(howHigh: 3) {
print(i)
}

/*
Prints the following, and finishes the loop:
1
2
3
*/



for await i in Counter(howHigh: 3) {
print(i)
if i == 2 { break }
}
/*
Prints the following:
1
2
*/

https://github.com/apple/swift-evolution/blob/main/proposals/0298-asyncsequence.md

SE-0299 在通用上下文中扩展静态成员查找

Swift 支持对具体类型的静态成员查找,并通过类似枚举的 . 语法来调用。例如,SwiftUI 中使用预定义的常用值作为静态属性扩展了 Font 和 Color 等类型。

extension Font {
public static let headline: Font
public static let subheadline: Font
public static let body: Font
...
}

extension Color {
public static let red: Color
public static let green: Color
public static let blue: Color
...
}

可通过以下方式来调用:

VStack {
Text(item.title)
.font(.headline)
.foregroundColor(.primary)
Text(item.subtitle)
.font(.subheadline)
.foregroundColor(.secondary)
}

不过,静态成员查找目前存在一个问题:不支持泛型函数中的协议成员,所以没有办法使用 . 语法来调用,如 SwiftUI 定义了一个 toggleStyle 视图装饰器:

extension View {
public func toggleStyle<S: ToggleStyle>(_ style: S) -> some View
}

public protocol ToggleStyle {
associatedtype Body: View
func makeBody(configuration: Configuration) -> Body
}

public struct DefaultToggleStyle: ToggleStyle { ... }
public struct SwitchToggleStyle: ToggleStyle { ... }
public struct CheckboxToggleStyle: ToggleStyle { ... }

目前的调用方式是,在使用 toggleStyle 修饰符时将具体类型以全名方式写入 ToggleStyle:

Toggle("Wi-Fi", isOn: $isWiFiEnabled)
.toggleStyle(SwitchToggleStyle())

而这个 proposal 的目标是放宽对协议上访问静态成员的限制,让泛型 API 具有更好的可读性,即通过以下方式来调用:

Toggle("Wi-Fi", isOn: $isWiFiEnabled)
.toggleStyle(.switch)

https://github.com/apple/swift-evolution/blob/main/proposals/0299-extend-generic-static-member-lookup.md

SE-0300 异步任务与同步代码接口

异步 Swift 代码需要能够与使用诸如完成回调和委托方法之类的技术的现有同步代码一起工作以响应事件。异步任务可以将自己暂停,然后同步代码可以捕获并调用它们以响应事件来恢复任务。

标准库将提供 API 来获取当前异步任务的延续,这会挂起任务,并产生一个值,同步代码随后可以通过该值使用句柄来恢复任务。例如

func beginOperation(completion: (OperationResult) -> Void)

我们可以通过挂起任务并在调用回调时使用其连续性将其恢复为异步接口,然后将传递给回调的参数转换为异步函数的正常返回值:

func operation() async -> OperationResult {
// Suspend the current task, and pass its continuation into a closure
// that executes immediately
return await withUnsafeContinuation { continuation in
// Invoke the synchronous callback-based API...
beginOperation(completion: { result in
// ...and resume the continuation when the callback is invoked
continuation.resume(returning: result)
})
}
}

https://github.com/apple/swift-evolution/blob/main/proposals/0300-continuation.md

SE-0304 结构化并发

async/await proposal 本向并没有引入并发性:它只是忽略异步函数中的挂起点,它将以与同步函数基本相同的方式执行。而这个 proposal 的目标就是在 Swift 中引入结构化并发的支持,并允许高效实现的模型来并发执行异步代码。

例如以下一段准备晚餐的代码:

func chopVegetables() async throws -> [Vegetable] { ... }
func marinateMeat() async -> Meat { ... }
func preheatOven(temperature: Double) async throws -> Oven { ... }

// ...

func makeDinner() async throws -> Meal {
let veggies = try await chopVegetables()
let meat = await marinateMeat()
let oven = try await preheatOven(temperature: 350)

let dish = Dish(ingredients: [veggies, meat])
return try await oven.cook(dish, duration: .hours(3))
}

makeDinner 中的每个步骤都是异步操作,不过整个流程每个点都会暂停,直到当前步骤完成。而为了更快地准备好晚餐,我们需要同时执行其中一些步骤,为此,可以将步骤分解为可以并行发生的不同任务。而这个 proposal 所提的结构化并发,就可以完成这种任务。proposal 中的概念很多,在此不详细介绍。在使用结构化并发对上述代码改造后,代码类似于以下:

func makeDinner() async throws -> Meal {
// Prepare some variables to receive results from our concurrent child tasks
var veggies: [Vegetable]?
var meat: Meat?
var oven: Oven?

enum CookingStep {
case veggies([Vegetable])
case meat(Meat)
case oven(Oven)
}

// Create a task group to scope the lifetime of our three child tasks
try await withThrowingTaskGroup(of: CookingStep.self)
{ group in
group.async {
try await .veggies(chopVegetables())
}
group.async {
await .meat(marinateMeat())
}
group.async {
try await .oven(preheatOven(temperature: 350))
}

for try await finishedStep in group {
switch finishedStep {
case .veggies(let v): veggies = v
case .meat(let m): meat = m
case .oven(let o): oven = o
}
}
}

// If execution resumes normally after `withTaskGroup`, then we can assume
// that all child tasks added to the group completed successfully. That means
// we can confidently force-unwrap the variables containing the child task
// results here.
let dish = Dish(ingredients: [veggies!, meat!])
return try await oven!.cook(dish, duration: .hours(3))
}

https://github.com/apple/swift-evolution/blob/main/proposals/0304-structured-concurrency.md

SE-0306 Actors

Swift 并发模型旨在提供一种安全的编程模型,该模型可静态检测数据竞争和其他常见的并发错误。结构化并发提议引入了一种定义并发任务的方法,并为函数和闭包提供了数据争用安全性。该模型适用于许多常见的设计模式,包括诸如并行映射和并发回调模式之类,但仅限于使用由闭包捕获的状态。

Swift 包含一些类,这些类提供了一种声明可变状态的机制,这些状态可以在程序之间共享。然而,类很难在并发程序中正确使用,需要手动同步以避免数据竞争。我们希望提供使用共享可变状态的功能,同时仍提供对数据竞争和其他常见并发错误的静态检测。

actor 模型定义了称为该角色的实体,非常适合此任务。Actor 允许声明并发域中包含的状态包,然后定义对其执行操作的多个操作。每个 actor 都通过数据隔离来保护自己的数据,从而确保即使在许多客户端同时发出参与者请求的情况下,在给定的时间也只有一个线程可以访问该数据。作为 Swift 并发模型的一部分,actor 提供了与结构化并发相同的竞争和内存安全属性。

actor 是一种引用类型,可保护对其可变状态的访问,并随关键字 actor 一起引入:

actor BankAccount {
let accountNumber: Int
var balance: Double

init(accountNumber: Int, initialDeposit: Double) {
self.accountNumber = accountNumber
self.balance = initialDeposit
}
}

像其他 Swift 类型一样,actor 可以有初始化器,方法,属性和下标。它们可以扩展并符合协议,可以是通用的,也可以与通用一起使用。

主要区别在于 actor 可以保护其状态免受数据争夺。这是 Swift 编译器通过强制使用 actor 及其实例成员的方式受到一系列限制而静态地强制实施的,统称为 actor 隔离。

https://github.com/apple/swift-evolution/blob/main/proposals/0306-actors.md

SE-0307 允许互换使用 CGFloat 和 Double 类型

Swift 首次发布时,CGFloat 的类型的使用就是一项挑战。当时,大多数 iOS 设备仍为 32 位。诸如 CoreGraphics 之类的 SDK 提供的 API 在 32 位平台上采用 32 位浮点值,在 64 位平台上采用 64 位值。首次引入这些 API 时,在 32 位平台上 32 位标量算术速度更快,但是到 Swift 发行时,情况已不再如此:直到今天, 64 位标量算术速度与 32 位一样快。甚至在 32 位平台上也是如此。之所以仍然保留了 32/64 位分割,主要是出于源和 ABI 稳定性的原因。

而这个 proposal 目标是允许 Double 和 CGFloat 类型通过将一种类型透明转换为另一种类型。

https://github.com/apple/swift-evolution/blob/main/proposals/0307-allow-interchangeable-use-of-double-cgfloat-types.md

SE-0308 #if 支持后缀成员表达式

Swift 有条件编译块 #if ... #endif,它允许根据一个或多个编译条件的值对代码进行条件编译。当前,与 C 语言中的 #if 不同的是,每个子句的主体必须包含完整的语句。但是,在某些情况下,尤其是在结果生成器上下文中,出现了将 #if 应用于部分表达式的需求。这个该 proposal 扩展了 #if ... #endif以便能够包围后缀成员表达式。

例如当前使用的如下代码:

VStack {
let basicView = Text("something")
#if os(iOS)
basicView
.iOSSpecificModifier()
.commonModifier()
#else
basicView
.commonModifier()
#endif
}

可以改成以下这种方式

VStack {
Text("something")
#if os(iOS)
.iOSSpecificModifier()
#endif
.commonModifier()
}

https://github.com/apple/swift-evolution/blob/main/proposals/0308-postfix-if-config-expressions.md

SE-0310 有效的只读属性

异步函数旨在用于可能会或始终会在返回之前暂停执行上下文切换的计算,但缺乏有效的只读计算属性和下标,因此这个 proposal 升级了 Swift 的只读属性以支持异步并单独或一起抛出关键字,从而使它们明显更灵活。

为了说明这一点,我们可以创建一个 BundleFile 结构体,尝试将其内容加载到应用程序的资源包中。由于文件可能不存在,或者可能存在但由于某种原因而无法读取,或者可能可读但太大,因此需要花费一些时间才能读取,因此我们可以将 contents 属性标记为异步抛出,如下所示:

enum FileError: Error {
case missing, unreadable
}

struct BundleFile {
let filename: String

var contents: String {
get async throws {
guard let url = Bundle.main.url(forResource: filename, withExtension: nil) else {
throw FileError.missing
}

do {
return try String(contentsOf: url)
} catch {
throw FileError.unreadable
}
}
}
}

因为 content 既是异步的又是抛出的,所以我们在尝试读取它时必须使用 try await:

func printHighScores() async throws {
let file = BundleFile(filename: "highscores")
try await print(file.contents)
}

https://github.com/apple/swift-evolution/blob/main/proposals/0310-effectful-readonly-properties.md

SE-0316 全局 actors

Actor 非常适合隔离实例数据,但是当需要隔离的数据分散在整个程序中,或者表示程序外部存在的某种状态时,将所有代码和数据都放入单个actor 实例中可能是不切实际的(例如,大型程序)甚至是不可能的(与那些假设无处不在的系统进行交互时)。

global actors 的主要目标是将 actor 模型应用于只能由主线程访问的状态和操作。在应用程序中,主线程通常负责执行主要的事件处理循环,该循环处理来自各种来源的事件并将其传递给应用程序代码。global actors 提供了一种机制,可以利用 actor 的角色来描述主线程,利用 Swift 的 actor 隔离模型来帮助正确使用主线程。

@MainActor var globalTextSize: Int

@MainActor func increaseTextSize() {
globalTextSize += 2 // okay:
}

func notOnTheMainActor() async {
globalTextSize = 12 // error: globalTextSize is isolated to MainActor
increaseTextSize() // error: increaseTextSize is isolated to MainActor, cannot call synchronously
await increaseTextSize() // okay: asynchronous call hops over to the main thread and executes there
}

https://github.com/apple/swift-evolution/blob/main/proposals/0316-global-actors.md

SE-0317 async let bindings

结构化并发提供了一个范式,用于在有范围的任务组中生成并发子任务,建立定义明确的任务层次结构,从而可以透明地处理并发管理的取消,错误传播,优先级管理和其他棘手的细节。

这个 proposal 旨在使用类似于 let 绑定的轻量级语法,使生成子任务的常规任务异步运行,并将最终结果传递给父任务。

还是以午餐为例,使用 async let,那么代码看起来是下面这种:

func makeDinner() async throws -> Meal {
async let veggies = chopVegetables()
async let meat = marinateMeat()
async let oven = preheatOven(temperature: 350)

let dish = Dish(ingredients: await [try veggies, meat])
return try await oven.cook(dish, duration: .hours(3))
}

https://github.com/apple/swift-evolution/blob/main/proposals/0317-async-let.md

小结

这里只是整理了部分在 Swift 5.5 中实现的 proposal 或者是在当前 main 快照中审核的 proposal。可以看到 async/await 及一些异步模型将会是 Swift 5.5 的重点内容。当然,最终 Swift 5.5 会新增哪些特性,还需要等最后的结果。也许 Swift 团队会将一些新特性放到 Swift 6 中发布。让我们期待一下 WWDC21 吧。



推荐阅读

☞  为 iPad 部署基于 VS Code 的远程开发环境
☞  “And away we code. ” WWDC21 超 200 个 Session 等着你
☞  Google 正式发布 Fuchsia OS,Flutter 集成尚存问题
☞  京东APP订单业务Swift优化总结


就差您点一下了 👇👇👇

浏览 56
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报