hiccLoghicc log by wccHipo日志

Swift 5.5 新特性

WWDC21上发布了Swift 5.5,虽然是小版本,但是特性不少……

Async/await#

SE-0296提案终于为开发者带来了期待已久的 async/await,语法基本上和javascript中的很像。

老的方式#

以前写异步函数像这样

func fetchWeatherHistory(completion: @escaping ([Double]) -> Void) { // Complex networking code here; we'll just send back 100,000 random temperatures DispatchQueue.global().async { let results = (1...100_000).map { _ in Double.random(in: -10...30) } completion(results) } } func calculateAverageTemperature(for records: [Double], completion: @escaping (Double) -> Void) { // Sum our array then divide by the array size DispatchQueue.global().async { let total = records.reduce(0, +) let average = total / Double(records.count) completion(average) } } func upload(result: Double, completion: @escaping (String) -> Void) { // More complex networking code; we'll just send back "OK" DispatchQueue.global().async { completion("OK") } }

这样调用

fetchWeatherHistory { records in calculateAverageTemperature(for: records) { average in upload(result: average) { response in print("Server response: \(response)") } } }

存在的问题是

  • 回调函数很容易调用多次,或者忘记调用。
  • 函数参数 @escaping (String) -> Void 看着也不直观
  • “回调地狱”看起来也不美观
  • 在Swift 5.0 增加了Result 类型之前,返回错误也困难。

Swift 5.5 Async/await方式#

func fetchWeatherHistory() async -> [Double] { (1...100_000).map { _ in Double.random(in: -10...30) } } func calculateAverageTemperature(for records: [Double]) async -> Double { let total = records.reduce(0, +) let average = total / Double(records.count) return average } func upload(result: Double) async -> String { "OK" }

使用也更简单

func processWeather() async { let records = await fetchWeatherHistory() let average = await calculateAverageTemperature(for: records) let response = await upload(result: average) print("Server response: \(response)") }

Async / await 错误处理#

swift 5.5 async 函数也可以像普通函数一样抛出错误 async throws

enum UserError: Error { case invalidCount, dataTooLong } func fetchUsers(count: Int) async throws -> [String] { if count > 3 { // Don't attempt to fetch too many users throw UserError.invalidCount } // Complex networking code here; we'll just send back up to `count` users return Array(["Antoni", "Karamo", "Tan"].prefix(count)) } func save(users: [String]) async throws -> String { let savedUsers = users.joined(separator: ",") if savedUsers.count > 32 { throw UserError.dataTooLong } else { // Actual saving code would go here return "Saved \(savedUsers)!" } }

使用也是类似

func updateUsers() async { do { let users = try await fetchUsers(count: 3) let result = try await save(users: users) print(result) } catch { print("Oops!") } }
👉函数加上async 并不会自动并行 concurrency

除非特殊处理,函数还是顺序执行,并不会自动并行,更不会自动跑在在其他线程。

Xcode 13 playground中运行异步代码#

现在(2021-7-25)之前,暂时还没有明显优雅的方式在playground中执行async / await 代码。参考这里,可以这样执行:

import Foundation struct Main { static func main() async { await doSomething() } static func doSomething() async { print("doSomething") } } Task.detached { await Main.main() exit(EXIT_SUCCESS) } RunLoop.main.run()

Async / await: sequences#

SE-0298提案为swift 引入了AsyncSequence protocol,循环异步队列。

使用AsyncSequence 使用和Sequence几乎一样。需要遵循AsyncSequenceAsyncIterator,当然next()方法需要时异步async的,和Sequence一样,迭代结束确认返回nil

struct DoubleGenerator: AsyncSequence { typealias Element = Int struct AsyncIterator: AsyncIteratorProtocol { var current = 1 mutating func next() async -> Int? { defer { current &*= 2 } if current < 0 { return nil } else { return current } } } func makeAsyncIterator() -> AsyncIterator { AsyncIterator() } }

运行也是常见的 for await 语法。

func printAllDoubles() async { for await number in DoubleGenerator() { print(number) } }

更高级的是,AsyncSequence协议提供了一些常用的方法,像map()compactMap(), allSatisfy()

func containsExactNumber() async { let doubles = DoubleGenerator() let match = await doubles.contains(16_777_216) print(match) }

read-only 属性#

SE-0310提案升级了swift的只读属性,让其支持了async和throws。

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 } } } }

使用自然像如下

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

Structured concurrency#

SE-0304在async / await 和async sequence的基础上为swift 引入了一整套的并发的执行,取消,监控的方法。

为了更好说明,我们先假设这两个情况

enum LocationError: Error { case unknown } func getWeatherReadings(for location: String) async throws -> [Double] { switch location { case "London": return (1...100).map { _ in Double.random(in: 6...26) } case "Rome": return (1...100).map { _ in Double.random(in: 10...32) } case "San Francisco": return (1...100).map { _ in Double.random(in: 12...20) } default: throw LocationError.unknown } } func fibonacci(of number: Int) -> Int { var first = 0 var second = 1 for _ in 0..<number { let previous = first first = second second = previous + first } return first }

在swift的项目中,可以这么执行(如果在playground文件中,可以使用上文的方法)

@main struct Main { static func main() async throws { let readings = try await getWeatherReadings(for: "London") print("Readings are: \(readings)") } }

结构化并发,实际上是引入了两个类型,TaskTaskGroup来,独立或者协同地运行并发代码。

简单来说,你只要将异步代码传入Task对象,就会立即在background 线程上运行,然后你用await等待结果就好。

func printFibonacciSequence() async { let task1 = Task { () -> [Int] in var numbers = [Int]() for i in 0..<50 { let result = fibonacci(of: i) numbers.append(result) } return numbers } let result1 = await task1.value print("The first 50 numbers in the Fibonacci sequence are: \(result1)") }

需要注意,代码中明确指定了Task { () -> [Int] in 这样,Swift就会知道task需要返回,而如果你的异步代码比较简单,可以像下面这样写:

let task1 = Task { (0..<50).map(fibonacci) }

再次,task会在创建之后立即运行,但是在斐波那契函数得到结果之后,,printFibonacciSequence()函数会继续运行在原来的线程。

👉task参数属于non-escaping 闭包

主要到task的函数参数中并没有标注@escape,因为task会立即执行传入的函数,而不是存储然后之后执行。因此,如果你在class或者struct中使用Task,你不需要self来获取属性和方法。

上述函数中,通过await task.value来获取task的值,如果你不关心task 返回的值,你也不需要存储task。

对于会抛出错误的异步任务,从task的value取值,也会触发错误,因此仍然需要try await

func runMultipleCalculations() async throws { let task1 = Task { (0..<50).map(fibonacci) } let task2 = Task { try await getWeatherReadings(for: "Rome") } let result1 = await task1.value let result2 = try await task2.value print("The first 50 numbers in the Fibonacci sequence are: \(result1)") print("Rome weather readings are: \(result2)") }

Swift为task内置了几种优先级highdefaultlowbackground。如果未指定,会默认设置为default,当然你可以显式指定Task(priority: .high)。当然如果你在Apple的平台上,你会使用很熟悉的优先级,userInitiated对应highutility对应low,当然你不能使用userInteractive,它是为主线程保留的。

当然,Task也为我们提供了几个静态的方法。

  • Task.sleep()会暂定当前任务一定时间(纳秒,也就是1_000_000_000为1秒)
  • Task.checkCancellation()会检查当前任务是否被cancel()取消,如果已经取消了,会抛出CancellationError错误。
  • Task.yield()会暂停当前任务一定时间,让给其他等待的任务让出点时间,这点在循环做一些繁重的任务会很有用。
func cancelSleepingTask() async { let task = Task { () -> String in print("Starting") await Task.sleep(1_000_000_000) try Task.checkCancellation() return "Done" } // The task has started, but we'll cancel it while it sleeps task.cancel() do { let result = try await task.value print("Result: \(result)") } catch { print("Task was cancelled.") } }

上述代码中,Task.checkCancellation()会检测到task已经被取消了,会立即跑出CancellationError的错误,当然只有在尝试task.value取值的时候,才会抛出。

👉使用task.result来获取到Result类型的值

你可以使用task.result来获取到Result类型的值,上述代码会返回Result<String, Error>。当然你也就不需要try来捕捉错误了。

对于复杂的任务,可以使用 task group来组织更多的task。

func printMessage() async { let string = await withTaskGroup(of: String.self) { group -> String in group.async { "Hello" } group.async { "From" } group.async { "A" } group.async { "Task" } group.async { "Group" } var collected = [String]() for await value in group { collected.append(value) } return collected.joined(separator: " ") } print(string) }

不要将withTaskGroup里面的代码复制到外面,编辑不会报错,但是自然会有潜在的问题。

所有task group中的任务应该返回同样的数据类型。复杂情况,你可能需要一个有associated值的enum来准确取值,当然你还可以使用async let 绑定的替代方案。

task group的值需要所有的task都完成,但是每个task执行完顺序是不保证的。

你可以在task group中处理错误,或者你可以使用withThrowingTaskGroup()把错误抛出,这样也就需要try的方式来取值。

func printAllWeatherReadings() async { do { print("Calculating average weather…") let result = try await withThrowingTaskGroup(of: [Double].self) { group -> String in group.async { try await getWeatherReadings(for: "London") } group.async { try await getWeatherReadings(for: "Rome") } group.async { try await getWeatherReadings(for: "San Francisco") } // Convert our array of arrays into a single array of doubles let allValues = try await group.reduce([], +) // Calculate the mean average of all our doubles let average = allValues.reduce(0, +) / Double(allValues.count) return "Overall average temperature is \(average)" } print("Done! \(result)") } catch { print("Error calculating data.") } }

上述每个 async任务,你可以简化使用for location in ["London", "Rome", "San Francisco"] {来调用。

task group 提供一个cancelAll()的方法来取消所有的task。之后你仍然可以给group添加异步任务。当然,你可以使用asyncUnlessCancelled()来跳过添加任务,如果group已经被取消—— 检查Boolean的返回值类判断group是否被取消。

async let 绑定#

SE-0317引入了一种更简易的语法async let来创建和等待子任务。这个可以作为task group的替代方法,特别是你需要使用不同数据类型的异步任务时候。

struct UserData { let username: String let friends: [String] let highScores: [Int] } func getUser() async -> String { "Taylor Swift" } func getHighScores() async -> [Int] { [42, 23, 16, 15, 8, 4] } func getFriends() async -> [String] { ["Eric", "Maeve", "Otis"] }

可以使用如下简单并发取值。

func printUserDetails() async { async let username = getUser() async let scores = getHighScores() async let friends = getFriends() let user = await UserData(name: username, friends: friends, highScores: scores) print("Hello, my name is \(user.name), and I have \(user.friends.count) friends!") }
👉你只能在async的上下文中,使用async let

你只能在async的上下文中,使用async let。而且如果你不去使用await取值,swift会在其作用于隐式等待。

绑定抛错的异步方法的时候,你也不需要使用try关键词。只需要取值时候try await

更高级的是,我们可以递归的使用async let语法。

enum NumberError: Error { case outOfRange } func fibonacci(of number: Int) async throws -> Int { if number < 0 || number > 22 { throw NumberError.outOfRange } if number < 2 { return number } async let first = fibonacci(of: number - 2) async let second = fibonacci(of: number - 1) return try await first + second }

Continuation函数(转换回调异步为async函数)#

SE-0300提供了把老的回调式异步函数转换为async函数的方法。

例如,有如下的回调函数

func fetchLatestNews(completion: @escaping ([String]) -> Void) { DispatchQueue.main.async { completion(["Swift 5.5 release", "Apple acquires Apollo"]) } }

swift 5.5之后,你不需要重写你的所有代码,你只需要使用withCheckedContinuation()函数包裹就好。

func fetchLatestNews() async -> [String] { await withCheckedContinuation { continuation in fetchLatestNews { items in continuation.resume(returning: items) } } }
  • resume(returning:)函数返回,你异步要返回的数据。
  • 确保resume(returning:)函数只调用一次。在withCheckedContinuation()函数中,swift会告警甚至会崩溃代码,当然这会有性能损耗。
  • 如果有更高性能要求,或者你确保你的代码不会有问题,你可以使用withUnsafeContinuation()

Actors#

SE-0306引入了actor,概念上和class很相似,但是swfit确保了,actor中的变量在任意时间段内只会被一个线程获取,这也就确保了actor在并发环境下的安全。

例如下面的代码

class RiskyCollector { var deck: Set<String> init(deck: Set<String>) { self.deck = deck } func send(card selected: String, to person: RiskyCollector) -> Bool { guard deck.contains(selected) else { return false } deck.remove(selected) person.transfer(card: selected) return true } func transfer(card: String) { deck.insert(card) } }

在单线程的环境中都是OK的。但是在多线程的环境中,我们代码就有了潜在的资源竞争风险,这也就导致了,当代码并行运行时,代码的执行结果会可能不同。

假设我们调用send(card:to:) 在同一时间调用多次,

  1. 第一个线程检查card是否在deck,存在,继续
  2. 第二个线程也检查card是否在deck,存在,也继续
  3. 第一个线程删除了deck中的card然后转移给了第二个人。
  4. 第二个线程尝试删除deck中的card,但是实际上已经不存在了,但是它还是把card转移给了另一个人。

这样就导致给一个人转移了两个卡片。绝对的麻烦。

Actor通过actor isolation隔离的方式解决这个问题:

  • 只能从外部异步地读取到actor的属性和方法,
  • 不能从外部写存储后的属性

swift 内部通过队列的方式避免资源竞争,因此应能不会很好。

对于上述的例子,我们可以改写为:

actor SafeCollector { var deck: Set<String> init(deck: Set<String>) { self.deck = deck } func send(card selected: String, to person: SafeCollector) async -> Bool { guard deck.contains(selected) else { return false } deck.remove(selected) await person.transfer(card: selected) return true } func transfer(card: String) { deck.insert(card) } }
  1. 通过actor关键词来创建一个Actor,这个是swift 新加的类型。
  2. send()方法被标为async,因为它需要一定时间来完成card转移。
  3. transfer(card:) 并没有标准为async,但是我们仍然需要await 来调用,因为需要等待SafeCollector actor能够处理请求。

更细节来说,actor内部可以任意读写属性和方法,但是和另一个actor交互的时候就必须使用异步的方式。这样就保证了线程安全,而且更棒的事编译后保证了这一点。

actor和class很像

  • 都是引用类型,因此它们可以被用来分享状态。
  • 都有方法,属性,构造器,和下标方法。
  • 可以遵循协议和泛型
  • 两者静态属性和方法都是一样的,因为它们没有self,因此也就不需要隔离。

当然actor相比class有两个最大的不同:

  1. Actor暂时不支持继承,因此它们的构造器initializer就简单多了——不需要convenience initializer,override,和final 关键词等。这个后续可能会改变。
  2. 所有的actor隐式的遵循了Actor的协议,其他的类型不能使用这个协议。
👉最好的actor描述:“actors pass messages, not memory.”

actor不是直接访问别的acotr内存,或者调用它们的方法,而是发送消息,让swift runtime来安全处理数据。

Global actors#

 SE-0316引入了全局actor来隔离全局状态避免数据竞争。

目前来说是引入了一个@MainActor来标柱装饰你的属性和方法,让其保证只在主线程运行。

对于app来说,UI更新就需要保证在主线程,以前的方式是使用DispatchQueue.main。swift 5.5之后,就简单了

class NewDataController { @MainActor func save() { print("Saving data…") } }
👉@MainActor标柱之后,必须异步调用。

就像上文,这里实际上是actor,因此,我们需要使用awaitasync let 等来调用save()

@MainActor底层是一个全局的actor底层是MainActor的结构体。其中有一个静态的run()方法来让我们代码在主线程中执行,而且也能够返回执行结果。

更多可以看,博主之前的文章:使用@MainActor自动在主线程更新UI

Sendable协议和@Sendable闭包#

SE-0302支持了“可传送”的数据,也就是可以安全的向另一个线程传送数据。也就是引入了新的Sendable protocol和装饰函数的@Sendable属性。

默认线程安全的有

  • 所有Swift核心的值类型, BoolIntString
  • 包裹的值类型的可选值(Optional)。
  • swift标准库值类型的集合,例如,Array<String> Dictionary<Int, String>
  • 值类型的元组(Tuple)
  • 元类型(Metatype),例如String.self

上述的值,都遵循了Sendable的协议。

而对于自定义的类型,如果满足下面的情况

  • Actor自动遵循Sendable,因为actor内部异步的处理数据。
  • 自定义的structenum,如果它们只包含遵循Sendable的值也会自动遵循Sendable,这点和Codable很像。
  • class能够遵循Sendable,如果1. 继承自NSObject,或者2. 不继承,而且所有的属性是常量的且能够遵循Sendable,还有类得标注为final来防止将来的继承。

Swift让函数和闭包,标注@Sendable,来能够并发调用。例如,Task的初始化函数标注了@Sendable,下面代码能够并发执行,因为其内捕获的是常量。

func printScore() async { let score = 1 Task { print(score) } Task { print(score) } }
👉如果score是变量,就不能在task内用了。

因为Task会并发执行,如果是变量,就存在数据竞争了。

你可以在自己代码标注@Sendable,这样也会强制上述的规则(值捕获)。

func runLater(_ function: @escaping @Sendable () -> Void) -> Void { DispatchQueue.global().asyncAfter(deadline: .now() + 3, execute: function) }

链式调用中支持#if语法#

SE-0308中Swift支持了在链式调用( postfix member expression)使用#if的条件判断表达式。这个乍看有点费解,其实是为了解决SwiftUI中根据条件添加view的修饰器。

Text("Welcome") #if os(iOS) .font(.largeTitle) #else .font(.headline) #endif

支持嵌套调用

#if os(iOS) .font(.largeTitle) #if DEBUG .foregroundColor(.red) #endif #else .font(.headline) #endif

当然,也不是非得在Swift UI中使用

let result = [1, 2, 3] #if os(iOS) .count #else .reduce(0, +) #endif print(result)
👉#if只能用在.操作

只有.操作才算是postfix member expression,所以#if不能用在 +[]等操作上。

CGFloat 和 Double 隐式转换#

SE-0307改进为开发带来了巨大的便利:Swift 能够在大多数情况下隐式转换CGFloatDouble

let first: CGFloat = 42 let second: Double = 19 let result = first + second print(result)

Swift会有限使用Double,更棒的是,不需要重写原来的代码,列入,Swift UI中的scaleEffect()仍然可以使用CGFloat,swift 内部转换为Double

Codable支持enum 关联值#

SE-0295升级了Swift Codable,让其能够支持枚举enum关联值。之前只有遵循rawRepresentable的enum才能使用Codable。

enum Weather: Codable { case sun case wind(speed: Int) case rain(amount: Int, chance: Int) }

现在能使用JSONEncoder的

let forecast: [Weather] = [ .sun, .wind(speed: 10), .sun, .rain(amount: 5, chance: 50) ] do { let result = try JSONEncoder().encode(forecast) let jsonString = String(decoding: result, as: UTF8.self) print(jsonString) } catch { print("Encoding error: \(error.localizedDescription)") } // [{"sun":{}},{"wind":{"speed":10}},{"sun":{}},{"rain":{"amount":5,"chance":50}}]

上面json key 可以使用CodingKey来自定义

函数中支持lazy关键词#

swift中lazy关键词能够让属性延迟求值,现在swift 5.5之后,函数中也能使用lazy关键词了。

func printGreeting(to: String) -> String { print("In printGreeting()") return "Hello, \(to)" } func lazyTest() { print("Before lazy") lazy var greeting = printGreeting(to: "Paul") print("After lazy") print(greeting) } lazyTest()

property wrapper 可以装饰到 function 和 closure 参数#

SE-0293扩展了property wrapper让其能够装饰到函数和闭包参数。

例如原来的函数

func setScore1(to score: Int) { print("Setting score to \(score)") } setScore1(to: 50) setScore1(to: -50) setScore1(to: 500) // Setting score to 50 // Setting score to -50 // Setting score to 500

使用property wrapper可以,用来固定score的参数。

@propertyWrapper struct Clamped<T: Comparable> { let wrappedValue: T init(wrappedValue: T, range: ClosedRange<T>) { self.wrappedValue = min(max(wrappedValue, range.lowerBound), range.upperBound) } }

现在使用的

func setScore2(@Clamped(range: 0...100) to score: Int) { print("Setting score to \(score)") } setScore2(to: 50) setScore2(to: -50) setScore2(to: 500) // Setting score to 50 // Setting score to 0 // Setting score to 100

扩展泛型函数中协议静态成员的查找#

SE-0299 提升了Swift对泛型函数中协议静态成员查找的能力。这点实际上也很能提升Swift UI的书写的便利。

以前需要这么写

Toggle("Example", isOn: .constant(true)) .toggleStyle(SwitchToggleStyle())

现在这么写,就很方便

Toggle("Example", isOn: .constant(true)) .toggleStyle(.switch)

跟具体来说,假设我们有如下的协议和结构体

protocol Theme { } struct LightTheme: Theme { } struct DarkTheme: Theme { } struct RainbowTheme: Theme { }

我们再定义一个Screen协议,有一个theme的泛型函数,来设置主题。

protocol Screen { } extension Screen { func theme<T: Theme>(_ style: T) -> Screen { print("Activating new theme!") return self } }

现在我们有个 Screen的结构体

struct HomeScreen: Screen { }

我们可以指定screen的主题为

let lightScreen = HomeScreen().theme(LightTheme())

现在Swift 5.5之后,我们可以在Theme协议上加个静态的属性

extension Theme where Self == LightTheme { static var light: LightTheme { .init() } }

现在Swift 5.5 设置主题就简单了

let lightTheme = HomeScreen().theme(.light)

Swift 3 到 Swift 5.4#