WWDC21上发布了Swift 5.5,虽然是小版本,但是特性不少……
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
看着也不直观Result
类型之前,返回错误也困难。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)")
}
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!")
}
}
👉函数加上**a
sync
** 并不会自动并行 concurrency
除非特殊处理,函数还是顺序执行,并不会自动并行,更不会自动跑在在其他线程。
现在(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()
SE-0298提案为swift 引入了AsyncSequence
protocol,循环异步队列。
使用AsyncSequence
使用和Sequence
几乎一样。需要遵循AsyncSequence
和AsyncIterator
,当然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)
}
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)
}
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)")
}
}
结构化并发,实际上是引入了两个类型,Task
和TaskGroup
来,独立或者协同地运行并发代码。
简单来说,你只要将异步代码传入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()
函数会继续运行在原来的线程。
👉**t
ask
**参数属于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内置了几种优先级high
, default
,low
和background
。如果未指定,会默认设置为default
,当然你可以显式指定Task(priority: .high)。当然如果你在Apple的平台上,你会使用很熟悉的优先级,userInitiated
对应high
,utility
对应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取值的时候,才会抛出。
👉使用**t
ask.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是否被取消。
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!")
}
👉你只能在**a
sync
**的上下文中,使用**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
}
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()。
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:)
在同一时间调用多次,
这样就导致给一个人转移了两个卡片。绝对的麻烦。
Actor通过actor isolation隔离的方式解决这个问题:
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)
}
}
actor
关键词来创建一个Actor,这个是swift 新加的类型。send()
方法被标为async
,因为它需要一定时间来完成card转移。transfer(card:)
并没有标准为async
,但是我们仍然需要await
来调用,因为需要等待SafeCollector
actor能够处理请求。更细节来说,actor内部可以任意读写属性和方法,但是和另一个actor交互的时候就必须使用异步的方式。这样就保证了线程安全,而且更棒的事编译后保证了这一点。
actor和class很像
self
,因此也就不需要隔离。当然actor相比class有两个最大的不同:
final
关键词等。这个后续可能会改变。👉最好的**a
ctor
**描述:“actors pass messages, not memory.”
actor不是直接访问别的acotr内存,或者调用它们的方法,而是发送消息,让swift runtime来安全处理数据。
SE-0316引入了全局actor来隔离全局状态避免数据竞争。
目前来说是引入了一个@MainActor
来标柱装饰你的属性和方法,让其保证只在主线程运行。
对于app来说,UI更新就需要保证在主线程,以前的方式是使用DispatchQueue.main
。swift 5.5之后,就简单了
class NewDataController {
@MainActor func save() {
print("Saving data…")
}
}
👉**@
MainActor
**标柱之后,必须异步调用。
就像上文,这里实际上是actor
,因此,我们需要使用await
,async let
等来调用save()
@MainActor
底层是一个全局的actor
底层是MainActor
的结构体。其中有一个静态的run()
方法来让我们代码在主线程中执行,而且也能够返回执行结果。
更多可以看,博主之前的文章:使用@MainActor自动在主线程更新UI。
SE-0302支持了“可传送”的数据,也就是可以安全的向另一个线程传送数据。也就是引入了新的Sendable
protocol和装饰函数的@Sendable
属性。
默认线程安全的有
Bool
,Int
,String
等Array<String>
, Dictionary<Int, String>
。String.self
上述的值,都遵循了Sendable
的协议。
而对于自定义的类型,如果满足下面的情况
Actor
自动遵循Sendable
,因为actor内部异步的处理数据。struct
和enum
,如果它们只包含遵循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) }
}
👉如果**s
core
**是变量,就不能在**task
**内用了。
因为Task
会并发执行,如果是变量,就存在数据竞争了。
你可以在自己代码标注@Sendable
,这样也会强制上述的规则(值捕获)。
func runLater(_ function: @escaping @Sendable () -> Void) -> Void {
DispatchQueue.global().asyncAfter(deadline: .now() + 3, execute: function)
}
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不能用在 +
,[]
等操作上。
SE-0307改进为开发带来了巨大的便利:Swift 能够在大多数情况下隐式转换CGFloat
和Double
。
let first: CGFloat = 42
let second: Double = 19
let result = first + second
print(result)
Swift会有限使用Double
,更棒的是,不需要重写原来的代码,列入,Swift UI中的scaleEffect()
仍然可以使用CGFloat
,swift 内部转换为Double
。
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}}]
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()
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)
Async/await, actors, throwing properties, and more!