2025-02-08 11:34:46
,某些文章具有时效性,若有错误或已失效,请在下方留言。Swift’s Task
struct lets us start running some work immediately, and optionally wait for the result to be returned. And it is optional: sometimes you don’t care about the result of the task, or sometimes the task automatically updates some external value when it completes, so you can just use them as “fire and forget” operations if you need to. This makes them a great way to run async code from a synchronous function.
Swift 的 Task
结构体让我们立即开始运行一些工作,并可选择等待返回结果。而且它 是 可选的 :有时你不关心任务的结果,或者有时任务在完成时会自动更新一些外部值,所以如果需要,你可以将它们用作 “即发即弃”作。这使它们成为从同步函数运行异步代码的好方法。
First, let’s look at an example where we create two tasks back to back, then wait for them both to complete. This will fetch data from two different URLs, decode them into two different structs, then print a summary of the results, all to simulate a user starting up a game – what are the latest news updates, and what are the current highest scores?
首先,让我们看一个示例,其中我们背靠背创建两个任务,然后等待它们都完成。这将从两个不同的 URL 获取数据,将它们解码为两个不同的结构,然后打印结果摘要,所有这些都是为了模拟用户启动游戏——最新的新闻更新是什么,当前的最高分是多少?
Here’s how that looks: 这是它的样子:
struct NewsItem: Decodable {
let id: Int
let title: String
let url: URL
}
struct HighScore: Decodable {
let name: String
let score: Int
}
func fetchUpdates() async {
let newsTask = Task {
let url = URL(string: "https://hws.dev/headlines.json")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([NewsItem].self, from: data)
}
let highScoreTask = Task {
let url = URL(string: "https://hws.dev/scores.json")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([HighScore].self, from: data)
}
do {
let news = try await newsTask.value
let highScores = try await highScoreTask.value
print("Latest news loaded with \(news.count) items.")
if let topScore = highScores.first {
print("\(topScore.name) has the highest score with \(topScore.score), out of \(highScores.count) total results.")
}
} catch {
print("There was an error loading user data.")
}
}
await fetchUpdates()
Let’s unpick the key parts:
让我们来了解一下关键部分:
- Creating and running a task is done by using its initializer, passing in the work you want to do.
创建和运行任务是通过使用其初始化器完成的,并传入要执行的工作。 - Tasks don’t always need to return a value, but when they do Swift can often figure it out automatically. If you have something more complex, you might need to declare it explicitly. For example, we might have used
() -> [NewsItem] in
to say that our task returns an array ofNewsItem
.
任务并不总是需要返回一个值,但是当它们返回值时,Swift 通常可以自动计算出来。如果你有更复杂的内容,你可能需要显式声明它。例如,我们可能已经使用了() -> [NewsItem] in
来表示我们的任务返回一个NewsItem
数组。 - As soon as you create the task it will start running – there’s no
start()
method or similar.
一旦你创建了任务,它就会开始运行 —— 没有start()
方法或类似方法。 - The entire task is run concurrently with your other code, which means it might be able to run in parallel too. In our case, that means fetching and decoding the data happens inside the task, which keeps our main
fetchUpdates()
function free.
整个任务与您的其他代码同时运行,这意味着它也可能能够并行运行。在我们的例子中,这意味着获取和解码数据发生在任务内部,这使我们的主fetchUpdates()
函数保持空闲。 - If you want to read the return value of a task, you need to access its
value
property usingawait
. In our case our task could also throw errors because we’re accessing the network, so we need to usetry
as well.
如果要读取任务的返回值,则需要使用await
访问其value
属性。在我们的例子中,我们的任务也可能引发错误,因为我们正在访问网络,因此我们也需要使用try
。 - Once you’ve copied out the value from your task you can use that normally without needing
await
ortry
again, although subsequent accesses to the task itself – e.g.newsTask.value
– will needtry await
because Swift can’t statically determine that the value is already present.
一旦你从任务中复制出值,你就可以正常使用它,而不需要await
或try
again,尽管对任务本身的后续访问(例如newsTask.value
) 将 需要try await
,因为 Swift 无法静态地确定该值已经存在。
Both tasks in that example returned a value, but that’s not a requirement – the “fire and forget” approach allows us to create a task without storing it, and Swift will ensure it runs until completion correctly.
该示例中的两个任务都返回了一个值,但这不是必需的 – “即发即弃”方法允许我们创建一个任务而不存储它,并且 Swift 将确保它正确运行直到完成。
To demonstrate this, we could make a small SwiftUI program to fetch a user’s inbox when a button is pressed. Button actions are not async functions, so we need to launch a new task inside the action. The task can call async functions, but in this instance we don’t actually care about the result so we’re not going to store the task – the function it calls will handle updating our SwiftUI view.
为了演示这一点,我们可以制作一个小型 SwiftUI 程序,以便在按下按钮时获取用户的收件箱。Button 动作 不是 异步函数,因此我们需要在 action 中启动一个新任务。任务 可以 调用异步函数,但在这种情况下,我们实际上并不关心结果,因此我们不会存储任务 – 它调用的函数将处理更新我们的 SwiftUI 视图。
Here’s the code: 这是代码:
struct Message: Decodable, Identifiable {
let id: Int
var from: String
var text: String
}
struct ContentView: View {
@State private var messages = [Message]()
var body: some View {
NavigationStack {
Group {
if messages.isEmpty {
Button("Load Messages") {
Task {
await loadMessages()
}
}
} else {
List(messages) { message in
VStack(alignment: .leading) {
Text(message.from)
.font(.headline)
Text(message.text)
}
}
}
}
.navigationTitle("Inbox")
}
}
func loadMessages() async {
do {
let url = URL(string: "https://hws.dev/messages.json")!
let (data, _) = try await URLSession.shared.data(from: url)
messages = try JSONDecoder().decode([Message].self, from: data)
} catch {
messages = [
Message(id: 0, from: "Failed to load inbox.", text: "Please try again later.")
]
}
}
}
Even though that code isn’t so different from the previous example, I still want to pick out a few things:
尽管该代码与前面的示例没有太大区别,但我仍然想挑选出几点:
- Creating the new task is what allows us to start calling an async function even though the button’s action is a synchronous function.
创建新任务允许我们开始调用异步函数,即使按钮的动作是同步函数。 - The lifetime of the task is not bound by the button’s action closure. So, even though the closure will finish immediately, the task it created will carry on running to completion.
任务的生命周期 不受 按钮的 action closure 的约束。因此,即使 Closure 将立即完成,它创建的 task 也会继续运行直到完成。 - We aren’t trying to read a return value from the task, or storing it anywhere. This task doesn’t actually return anything, and doesn’t need to.
我们不会尝试从 task 中读取返回值,或将其存储在任何地方。此任务实际上不会返回任何内容,也不需要返回任何内容。
I know it’s a not a lot of code, but between Task
, async/await, and SwiftUI a lot of work is happening on our behalf. Remember, when we use await
we’re signaling a potential suspension point, which means the task might sleep for a while based on the work that’s happening.
我知道这并不是很多代码,但在 Task
、async/await 和 SwiftUI 之间,很多工作都是代表我们进行的。请记住,当我们使用 await
时,我们是在发出潜在暂停点的信号,这意味着根据正在进行的工作,任务可能会休眠一段时间。
Let’s break it down: 让我们来分析一下:
- All UI work runs on the main thread, so the button’s action closure will fire on the main thread.
所有 UI 工作都在主线程上运行,因此按钮的 action closure 将在主线程上触发。 - We create the task on the main thread, and the code we’re running belongs to our SwiftUI view, so it will also run on the main thread.
我们在主线程上创建任务,我们运行的代码属于我们的 SwiftUI 视图,因此它也将在主线程上运行。 - Inside
loadMessages()
we useawait
to load our URL data, but that will run on its own networking thread to avoid making our UI freeze. When it resumes, our code will return to the main thread.
在loadMessages()
中,我们使用await
来加载我们的 URL 数据,但它将在自己的网络线程上运行,以避免使我们的 UI 冻结。当它恢复时,我们的代码将返回到主线程。 - Finally, the
messages
property uses the@State
property wrapper, which will automatically update its value on the main thread no matter where we change it from.
最后,messages
属性使用@State
属性包装器,无论我们从何处更改它,它都会在主线程上自动更新其值。
Best of all, we don’t have to care about this – we don’t need to know how the system is balancing the threads, or even that the threads exist, because Swift and SwiftUI take care of that for us. In fact, the concept of tasks is so thoroughly baked into SwiftUI that there’s a dedicated task()
modifier that makes them even easier to use.
最重要的是,我们不必关心这些 – 我们不需要知道系统是如何平衡线程的,甚至不需要知道线程的存在,因为 Swift 和 SwiftUI 会为我们处理这些。事实上,任务的概念已经完全融入到 SwiftUI 中,以至于有一个专用的 task()
修饰符,使它们更易于使用。
暂无评论内容