建立一個可變迭代器,並針對每個後續值呼叫 await iterator.next()。進行簡短的 Mac 命令列計算時,請將串流和迭代器都保留在重複讀取操作之外,並將 nil 視為完成。
本逐步說明會使用合成整數 4、7 和 10,將每個值平方,並確認總和為 165。計算只會使用記憶體中建立的值。此範例旨在透過一個小程式,清楚呈現迭代器的生命週期、緩衝和完成狀態。
值重複時,究竟是什麼重新開始?
Swift 將非同步序列與其迭代器分開。序列會透過 makeAsyncIterator() 提供迭代器,而迭代器則透過呼叫 next() 向前推進。每次呼叫都會傳回一個元素;若迭代已結束,則傳回 nil。Apple 在 AsyncIteratorProtocol 中說明了這項契約。
當應用程式重複收到第一個值時,有兩種不同操作需要檢查:建立另一個迭代器,以及建立另一個串流。兩者不可混為一談。在目前的標準函式庫實作中,AsyncStream 迭代器持有串流的共用內容,而 makeAsyncIterator() 會將現有內容傳入迭代器。因此,在同一個串流上建立另一個迭代器,不會建立一份可從頭重播的獨立副本。請參閱官方 AsyncStream 實作。
相較之下,呼叫建立新串流的工廠,並產生 4、7 和 10,會建立另一個包含這些值的來源。如果每次讀取都呼叫該工廠,並且只取用一個元素,每次讀取都可能收到 4。在 getter 中建立串流的計算屬性也可能造成相同效果。就這項工作而言,請只建立一次來源,並在整個計算過程中保留同一個迭代器。
決定將程式碼移到何處時,請運用這項區分:將串流建立移到重複操作之外,以保留來源;將迭代器建立移到操作之外,以維持單一且明確的取用路徑。不要把重複建立迭代器當成重新開始的機制,也不要用它來維持進度。
在 Mac 上執行完整的本機計算
請使用安裝 Swift 6 或更新工具鏈的 Mac。在「終端機」中檢查已安裝的編譯器,接著在新目錄中建立小型可執行套件。Swift.org 的建置命令列工具指南說明了版本檢查、建立套件和 swift run 工作流程。
swift --version
mkdir IteratorDemo
cd IteratorDemo
swift package init --name IteratorDemo --type executable
在 Sources 下找到產生的可執行檔原始碼。將內容替換為下方完整程式。將該原始碼檔命名為 IteratorDemo.swift;如果範本建立了 main.swift,請將它重新命名並移除原有內容。只保留一個可執行進入點。Swift.org 說明,同一個可執行檔不能同時使用 main.swift 進入點和 @main 進入點。
此範例不需要匯入任何模組:AsyncStream 屬於 Swift,而 Foundation 是獨立的框架。此計算不需要 Foundation API。Apple 的 AsyncStream 說明文件和 Foundation 概覽說明了這些界線。
@main
struct IteratorDemo {
static func makeValues() -> AsyncStream<Int> {
AsyncStream<Int>(bufferingPolicy: .bufferingOldest(3)) {
continuation in
for value in [4, 7, 10] {
let result = continuation.yield(value)
guard case .enqueued = result else {
preconditionFailure("A sample value was not enqueued")
}
}
continuation.finish()
}
}
static func main() async {
let stream = makeValues()
var iterator = stream.makeAsyncIterator()
var received: [Int] = []
var squareTotal = 0
for readNumber in 1...3 {
guard let value = await iterator.next() else {
preconditionFailure("Stream ended before read \(readNumber)")
}
received.append(value)
squareTotal += value * value
print("Read \(readNumber): \(value)")
}
let end = await iterator.next()
let stillEnded = await iterator.next()
precondition(received == [4, 7, 10])
precondition(squareTotal == 165)
precondition(end == nil)
precondition(stillEnded == nil)
print("Square total: \(squareTotal)")
print("Finished: \(end == nil)")
}
}
在套件目錄中執行可執行檔:
swift run IteratorDemo
應用程式預期輸出如下(可能會先出現建置訊息):
Read 1: 4
Read 2: 7
Read 3: 10
Square total: 165
Finished: true
這些算術運算只是明確的示範:4 × 4 + 7 × 7 + 10 × 10 = 165。這些斷言會檢查精確值、值的順序、計算結果和終止行為。研究環境中沒有 Swift 編譯器,因此這裡只檢視了程式,並未執行。
為什麼這個迭代器能正確持續前進?
程式會呼叫一次 makeValues(),並呼叫一次 makeAsyncIterator()。三次讀取都使用同一個可變變數。Apple 將迭代器的 next() 方法宣告為可變且非同步,這也說明了為何使用 var iterator 和 await iterator.next()。
產生器會在串流建立期間、開始取用之前產生三個值。因此,緩衝區容量是此範例契約的一部分:.bufferingOldest(3) 足以容納三個整數。檢查每次 yield 的結果,可讓非預期的佇列失敗清楚顯現,避免計算在缺少值的情況下繼續進行。Apple 說明,若當下沒有等待中的讀取,yield(_:) 會嘗試緩衝一個值。
在這些值產生之後呼叫 finish(),會關閉產生流程,同時允許取用已緩衝的值。Swift 已接受的提案明確指出,緩衝元素會在終止狀態 nil 傳達給消費端之前送達。因此,即使產生器已經完成,前三次讀取仍會成功。請參閱 AsyncStream 和 AsyncThrowingStream,SE-0314。
第四次讀取會發現串流已完成且耗盡,並傳回 nil。第五次讀取則確認終止契約:迭代器一旦傳回 nil,後續讀取也必須傳回 nil。不要將 nil 解讀為稍後重試或重建迭代器的指示;AsyncIteratorProtocol 將其定義為迭代結束。
選擇明確讀取或單一連續迴圈
當周邊操作需要在個別讀取之間進行控制時(如這個編號計算),請使用明確的 next() 呼叫。將迭代器保留在負責整個取用工作階段的作用域中。每次呼叫都自行建立迭代器的輔助函式,會模糊其所有權;若輔助函式也重建串流,就會改變來源本身。
如果任務只是處理每個元素直到完成,請使用單一 for await 迴圈。Apple 的 AsyncStream 概覽示範了這種支援的取用方式。對於範例程式,以下片段可取代現有 main() 方法的全部內容;保留現有的 makeValues() 方法和外圍型別:
static func main() async {
let stream = makeValues()
var received: [Int] = []
var squareTotal = 0
for await value in stream {
received.append(value)
squareTotal += value * value
}
precondition(received == [4, 7, 10])
precondition(squareTotal == 165)
print("Square total: \(squareTotal)")
}
此版本表達單一且連續的取用工作階段。若不需要個別讀取命令,請選用此寫法。若個別讀取是任務結構的一部分,則選用明確的迭代器。無論採用哪種方式,都需要正確建立來源並定義結束條件。
診斷重複、缺少或未完成的讀取
以下步驟可區分來源生命週期問題、緩衝問題和完成問題。在加入更多行為之前,請先套用到固定範例。
- 重複的
4:檢查來源建立方式。 確認每次操作是否都再次呼叫makeValues(),或是否存取了會建立新串流的 getter。請將工廠呼叫移到重複讀取之外。 - 不同消費端收到非預期值:檢查所有權。 讓這項計算只使用一個消費端和一個迭代器。同一串流上的多個迭代器不會提供互相獨立的值副本。
- 缺少值:檢查緩衝區容量。 此範例會在開始取用前產生全部三個整數。較小的緩衝區會改變哪些值能保留下來。
- 最後一次讀取一直無法完成:檢查完成狀態。 在以 continuation 為基礎的串流中,緩衝區為空本身不會結束產生流程。請確認有限產生器會呼叫
finish()。 - 讀取重疊:將取用操作序列化。 在開始對迭代器進行下一次讀取前,先 await 前一次讀取。
在此範例的條件下,變更緩衝區會產生可預測的結果。使用 .bufferingOldest(2) 時,前兩個值會保留,新值 10 則會被丟棄。使用 .bufferingNewest(2) 時,最舊的緩衝值 4 會被丟棄,留下 7 和 10。這些結果是依據 Apple 說明的 bufferingOldest(_:) 和 bufferingNewest(_:) 政策,從上述產生順序推導而來。
這兩種政策都不會讓產生器等待取用。Apple 說明,yield(_:) 會立即傳回,因此保留同一個迭代器也無法彌補緩衝區容量不足。對於這個包含三個元素的有限計算,容量設為三便足夠;不同的產生模式需要另行決定容量。
請依序存取迭代器。Apple 指出,AsyncStream.Iterator 不符合 Sendable,同時從多個競爭的並行呼叫 next() 也屬於程式設計錯誤。如果消費任務在等待值時遭取消,也會影響 continuation;Apple 的 next() 說明文件指出,串流會終止。重建另一個迭代器並非文件所述的恢復已取消串流的方法。
對於這項本機計算,完整的取用契約很簡單:建立一個來源,保留一個可變迭代器,依序 await 讀取,在緩衝區中保留全部三個值,並在 nil 時停止。這些選擇讓進度一目了然,也讓最終總和有可驗證的意義。
來源
- AsyncStream — Apple。
- AsyncIteratorProtocol — Apple。
- AsyncStream.Iterator — Apple。
- next() — Apple。
- yield(_:) — Apple。
- AsyncStream.Continuation.BufferingPolicy.bufferingOldest(_:) — Apple。
- AsyncStream.Continuation.BufferingPolicy.bufferingNewest(_:) — Apple。
- Foundation — Apple。
- 建置命令列工具 — Swift.org。
- AsyncStream.swift — Swift 專案。
- AsyncStream 和 AsyncThrowingStream,SE-0314 — Swift 專案。