2025-02-08 10:20:28
,某些文章具有时效性,若有错误或已失效,请在下方留言。There are only three differences between creating an AsyncSequence
and creating a regular Sequence
, none of which are complicated.
创建 AsyncSequence
和创建常规 Sequence
之间只有三个区别,没有一个很复杂。
The differences are: 区别在于:
- We need to conform to the protocols
AsyncSequence
andAsyncIteratorProtocol
.
我们需要遵守协议AsyncSequence
和AsyncIteratorProtocol
。 - The
next()
method of our iterator must be markedasync
.
迭代器的next()
方法必须标记为async
。 - We need to create a
makeAsyncIterator()
method rather thanmakeIterator()
.
我们需要创建一个makeAsyncIterator()
方法,而不是makeIterator()。
That last point technically allows us to create one type that is both a synchronous and asynchronous sequence, although I’m not sure when that would be a good idea.
从技术上讲,最后一点允许我们创建一个既是同步序列又是异步序列的类型,尽管我不确定何时这是一个好主意。
We’re going to build two async sequences so you can see how they work, one simple and one more realistic. First, the simple one, which is an async sequence that doubles numbers every time next()
is called:
我们将构建两个异步序列,以便您了解它们的工作原理,一个简单,一个更真实。首先是简单的,它是一个异步序列,每次调用 next()
时都会使数字加倍:
struct DoubleGenerator: AsyncSequence, AsyncIteratorProtocol {
var current = 1
/* 结构体和枚举是 值类型。默认情况下,值类型的属性不能在其实例方法内部被修改。
在特定方法内部修改结构体或枚举的属性,可以为该方法启用 mutating 行为 */
mutating func next() async -> Int? {
defer { current &*= 2 }
if current < 0 {
return nil
} else {
return current
}
}
func makeAsyncIterator() -> DoubleGenerator {
self
}
}
let sequence = DoubleGenerator()
for await number in sequence {
print(number)
}
Tip: In case you haven’t seen it before, &*=
multiplies with overflow, meaning that rather than running out of room when the value goes beyond the highest number of a 64-bit integer, it will instead flip around to be negative. We use this to our advantage, returning nil
when we reach that point.
提示: 如果您以前没有见过它,&*=
与 overflow 相乘,这意味着当值超过 64 位整数的最大数字时,它不会耗尽空间,而是会翻转为负数。我们利用这一点,当我们到达该点时返回 nil
。
If you prefer having a separate iterator struct, that also works as with Sequence
and you don’t need to adjust the calling code:
如果你更喜欢使用单独的迭代器结构体,它也与 Sequence
一样工作,并且不需要调整调用代码:
struct DoubleGenerator: AsyncSequence {
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()
}
}
let sequence = DoubleGenerator()
for await number in sequence {
print(number)
}
Now let’s look at a more complex example, which will periodically fetch a URL that’s either local or remote, and send back any values that have changed from the previous request.
现在让我们看一个更复杂的示例,它将定期获取本地或远程 URL,并发回与上一个请求相比已更改的任何值。
This is more complex for various reasons:
由于各种原因,这更复杂:
- Our
next()
method will be markedthrows
, so callers are responsible for handling loop errors.
我们的next()
方法将被标记为throws
,因此调用者负责处理循环错误。 - Between checks we’re going to sleep for some number of seconds, so we don’t overload the network. This will be configurable when creating the watcher, but internally it will use
Task.sleep()
.
在检查之间,我们将休眠几秒钟,这样我们就不会使网络过载。这将在创建 watcher 时进行配置,但在内部它将使用Task.sleep()。
- If we get data back and it hasn’t changed, we go around our loop again – wait for some number of seconds, re-fetch the URL, then check again.
如果我们取回数据并且数据没有改变,我们就会再次循环——等待几秒钟,重新获取 URL,然后再次检查。 - Otherwise, if there has been a change between the old and new data, we overwrite our old data with the new data and send it back.
否则,如果旧数据和新数据之间发生更改,我们将用新数据覆盖旧数据并将其发送回去。 - If no data is returned from our request, we immediately terminate the iterator by sending back
nil
.
如果我们的请求没有返回任何数据,我们会立即通过发回nil
来终止迭代器。 - This is important: once our iterator ends, any further attempt to call
next()
must also returnnil
. This is part of the design ofAsyncSequence
, so stick to it.
这一点很重要:一旦我们的迭代器结束,任何进一步调用next()
的尝试也必须返回nil
。这是AsyncSequence
设计的一部分,因此请坚持使用。
Like I said, this is more complex, but it’s also a useful, real-world example of AsyncSequence
. It’s also particularly powerful when combined with SwiftUI’s task()
modifier, because the network fetches will automatically start when a view is shown and cancelled when it disappears. This allows you to constantly watch for new data coming in, and stream it directly into your UI.
就像我说的,这更复杂,但它也是 AsyncSequence
的一个有用的真实示例。当与 SwiftUI 的 task()
修饰符结合使用时,它也特别强大,因为网络获取将在视图显示时自动启动,并在视图消失时取消。这使您可以持续监视传入的新数据,并将其直接流式传输到您的 UI 中。
Anyway, here’s the code – it creates a URLWatcher
struct that conforms to the AsyncSequence
protocol, along with an example of it being used to display a list of users in a SwiftUI view:
无论如何,这是代码 – 它创建了一个符合 AsyncSequence
协议的 URLWatcher
结构,以及一个用于在 SwiftUI 视图中显示用户列表的示例:
struct URLWatcher: AsyncSequence, AsyncIteratorProtocol {
let url: URL
let delay: Int
private var comparisonData: Data?
private var isActive = true
init(url: URL, delay: Int = 10) {
self.url = url
self.delay = delay
}
mutating func next() async throws -> Data? {
// Once we're inactive always return nil immediately
guard isActive else { return nil }
if comparisonData == nil {
// If this is our first iteration, return the initial value
comparisonData = try await fetchData()
} else {
// Otherwise, sleep for a while and see if our data changed
while true {
try await Task.sleep(for: .seconds(delay))
let latestData = try await fetchData()
if latestData != comparisonData {
// New data is different from previous data,
// so update previous data and send it back
comparisonData = latestData
break
}
}
}
if comparisonData == nil {
isActive = false
return nil
} else {
return comparisonData
}
}
private func fetchData() async throws -> Data {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
func makeAsyncIterator() -> URLWatcher {
self
}
}
// As an example of URLWatcher in action, try something like this:
struct User: Identifiable, Decodable {
let id: Int
let name: String
}
struct ContentView: View {
@State private var users = [User]()
var body: some View {
List(users) { user in
Text(user.name)
}
.task {
// continuously check the URL watcher for data
await fetchUsers()
}
}
func fetchUsers() async {
let url = URL(fileURLWithPath: "FILENAMEHERE.json")
let urlWatcher = URLWatcher(url: url, delay: 3)
do {
for try await data in urlWatcher {
try withAnimation {
users = try JSONDecoder().decode([User].self, from: data)
}
}
} catch {
// just bail out
}
}
}
To make that work in your own project, replace “FILENAMEHERE” with the location of a local file you can test with.
要在您自己的项目中实现该工作,请将 “FILENAMEHERE” 替换为您可以测试的本地文件的位置。
Important: If you’re trying this out with a local file on macOS, go to the Signing & Capabilities tab for your target then remove the App Sandbox capability so you can read files anywhere.
重要: 如果你在macOS上尝试使用本地文件进行此作,请转到目标的签名和功能选项卡,然后删除应用沙盒功能,以便你可以在任何地方读取文件。
For example, I might use /Users/twostraws/Desktop/users.json, giving that file the following example contents:
例如,我可能会使用 /Users/twostraws/Desktop/users.json,为该文件提供以下示例内容:
[
{
"id": 1,
"name": "Paul"
}
]
When the code first runs the list will show Paul, but if you edit the JSON file and re-save with extra users, they will just slide into the SwiftUI list automatically.
当代码首次运行时,列表将显示 Paul,但如果您编辑 JSON 文件并与额外用户一起重新保存,他们将自动滑入 SwiftUI 列表。