2025-02-07 10:15:32
,某些文章具有时效性,若有错误或已失效,请在下方留言。Older Swift code uses completion handlers for notifying us when some work has completed, and sooner or later you’re going to have to use it from an async
function – either because you’re using a library someone else created, or because it’s one of your own functions but updating it to async would take a lot of work.
较旧的 Swift 代码使用补全处理程序来通知我们何时完成某些工作,您迟早将不得不从 async
函数中使用它 —— 要么是因为您正在使用其他人创建的库,要么因为它是您自己的函数之一,但将其更新为 async 需要大量工作。
Swift uses continuations to solve this problem, allowing us to create a bridge between older functions with completion handlers and newer async code.
Swift 使用 continuations 来解决这个问题,允许我们在带有补全处理程序的旧函数和较新的异步代码之间架起一座桥梁。
To demonstrate this problem, here’s some code that attempts to fetch some JSON from a web server, decode it into an array of Message
structs, then send it back using a completion handler:
为了演示这个问题,下面是一些代码,它尝试从 Web 服务器获取一些 JSON,将其解码为 Message
结构数组,然后使用完成处理程序将其发送回去:
struct Message: Decodable, Identifiable {
let id: Int
let from: String
let message: String
}
func fetchMessages(completion: @Sendable @escaping ([Message]) -> Void) {
let url = URL(string: "https://hws.dev/user-messages.json")!
URLSession.shared.dataTask(with: url) { data, response, error in
if let data {
if let messages = try? JSONDecoder().decode([Message].self, from: data) {
completion(messages)
return
}
}
completion([])
}.resume()
}
Although the dataTask(with:)
method does run our code on its own thread, this is not an async function in the sense of Swift’s async/await feature, which means it’s going to be messy to integrate into other code that does use modern async Swift.
尽管 dataTask(with:)
方法确实在它自己的线程上运行我们的代码,但这不是 Swift 的 async/await 功能意义上的异步函数,这意味着集成到其他使用现代异步 Swift 的代码中会很麻烦。
To fix this, Swift provides us with continuations, which are special objects we pass into the completion handlers as captured values. Once the completion handler fires, we can either return the finished value, throw an error, or send back a Result
that can be handled elsewhere. There’s no time limit on how long continuations can take to complete, but they must complete at some point.
为了解决这个问题,Swift 为我们提供了 continuations,它们是我们作为捕获值传递给补全处理程序的特殊对象。一旦完成处理程序触发,我们可以返回 finished 值、抛出错误或发送回可以在其他地方处理的 Result
。continuations 完成需要多长时间没有时间限制,但它们必须在某个时间点完成。
In the case of fetchMessages()
, we want to write a new async function that calls the original, and in its completion handler we’ll return whatever value was sent back:
对于 fetchMessages(),
我们想要编写一个新的异步函数来调用原始函数,并在其完成处理程序中返回发回的任何值:
struct Message: Decodable, Identifiable {
let id: Int
let from: String
let message: String
}
func fetchMessages(completion: @Sendable @escaping ([Message]) -> Void) {
let url = URL(string: "https://hws.dev/user-messages.json")!
URLSession.shared.dataTask(with: url) { data, response, error in
if let data {
if let messages = try? JSONDecoder().decode([Message].self, from: data) {
completion(messages)
return
}
}
completion([])
}.resume()
}
func fetchMessages() async -> [Message] {
await withCheckedContinuation { continuation in
fetchMessages { messages in
continuation.resume(returning: messages)
}
}
}
let messages = await fetchMessages()
print("Downloaded \(messages.count) messages.")
As you can see, starting a continuation is done using the withCheckedContinuation()
function, which passes into itself the continuation we need to work with. It’s called a “checked” continuation because Swift checks that we’re using the continuation correctly, which means abiding by one very simple, very important rule:
如你所见,启动 continuation 是使用 withCheckedContinuation()
函数完成的,该函数将我们需要使用的 continuation 传递给自身。它被称为 “checked” continuation,因为 Swift 会检查我们是否正确使用了 continuation,这意味着要遵守一个非常简单、非常重要的规则:
Your continuation must be resumed exactly once. Not zero times, and not twice or more times – exactly once. Again, there’s no time limit on this happening, but it must happen at some point.
您的延续必须只恢复一次。不是零次,也不是两次或更多次 – 恰好一次 。 同样,这种情况没有时间限制,但它必须在某个时候发生。
If you call the checked continuation twice or more, Swift will cause your program to halt – it will just crash. I realize this sounds bad, but when the alternative is to have some bizarre, unpredictable behavior, crashing doesn’t sound so bad.
如果您调用已检查的延续两次或更多次,Swift 将导致您的程序停止 – 它只会崩溃。我知道这听起来很糟糕,但是当替代方案是出现一些奇怪的、不可预测的行为时,崩溃听起来并不那么糟糕。
On the other hand, if you fail to resume the continuation at all, Swift will print out a large warning in your debug log similar to this: “SWIFT TASK CONTINUATION MISUSE: fetchMessages() leaked its continuation!” This is because you’re leaving the task suspended, causing any resources it’s using to be held indefinitely.
另一方面,如果您根本无法恢复延续,Swift 将在您的调试日志中打印出一个类似于以下内容的大警告:“SWIFT TASK CONTINUATION MISUSE: fetchMessages() 泄露了它的延续!这是因为您将任务置于暂停状态,从而导致它使用的任何资源被无限期保留。
You might think these are easy mistakes to avoid, but in practice they can occur in all sorts of places if you aren’t careful.
您可能认为这些错误很容易避免,但在实践中,如果您不小心,它们可能会发生在各种地方。
For example, in our original fetchMessages()
method we used this:
例如,在我们最初的 fetchMessages()
方法中,我们使用了这个:
if let data {
if let messages = try? JSONDecoder().decode([Message].self, from: data) {
completion(messages)
return
}
}
completion([])
That checks for data coming back, and checks that it can be decoded correctly, before completing and returning, but if either of those two checks fail then the completion handler is called with an empty array – no matter what happens, the completion handler gets called.
它会检查返回的数据,并在完成和返回之前检查是否可以正确解码,但如果这两项检查中的任何一项失败,则使用空数组调用完成处理程序 – 无论发生什么,都会调用完成处理程序。
But what if we had written something different? See if you can spot the problem with this alternative:
但是,如果我们写了一些不同的东西呢?看看您是否能发现此替代方案的问题:
if let data {
if let messages = try? JSONDecoder().decode([Message].self, from: data) {
completion(messages)
}
} else {
completion([])
}
That attempts to decode the JSON into a Message
array and send back the result using the completion handler, or send back an empty array if nothing came back from the server.
这将尝试将 JSON 解码为 Message
数组并使用完成处理程序发回结果,或者如果服务器没有返回任何内容,则发回空数组。
However, it has a mistake that will cause problems with continuations: if some valid data comes back but can’t be decoded into an array of messages, the completion handler will never be called and our continuation will be leaked.
但是,它有一个错误,会导致 continuations 出现问题:如果一些有效数据返回但无法 解码为消息数组,则永远不会调用完成处理程序,并且我们的 continuation 将被泄漏。
These two code samples are fairly similar, which shows how important it is to be careful with your continuations. However, if you have checked your code carefully and you’re sure it is correct, you can if you want replace the withCheckedContinuation()
function with a call to withUnsafeContinuation()
, which works exactly the same way but doesn’t add the runtime cost of checking you’ve used the continuation correctly.
这两个代码示例非常相似,这表明小心使用 continuation 是多么重要。但是,如果您已经仔细检查了您的代码并确定它是正确的,那么如果您希望将 withCheckedContinuation()
函数替换为对 withUnsafeContinuation()
的调用,您可以这样做,其工作方式完全相同,但不会增加检查您是否正确使用了延续的运行时成本。
I say you can do this if you want, but I’m dubious about the benefit. It’s easy to say “I know my code is safe, go for it!” but I’d be wary about moving across to unsafe code unless you had profiled your code using Instruments and were quite sure Swift’s extra checks were causing a performance problem.
我说如果你愿意 ,你可以这样做,但我对它的好处持怀疑态度。说“我知道我的代码是安全的,就去做吧”很容易,但是我会对转向不安全的代码持谨慎态度,除非你已经使用 Instruments 分析了你的代码,并且非常确定 Swift 的额外检查导致了性能问题。
Note: If you want a continuation that sends back nothing, you can just use continuation.resume()
with no parameters.
注意: 如果你想要一个不发回任何内容的 continuation,你可以只使用 continuation.resume()
而不带参数。