Buat satu iterator yang dapat diubah dan panggil await iterator.next() untuk setiap nilai berikutnya. Untuk kalkulasi kecil di baris perintah Mac, tempatkan stream dan iterator di luar operasi pembacaan berulang, dan perlakukan nil sebagai tanda selesai.
Panduan ini menggunakan bilangan bulat sintetis 4, 7, dan 10, menguadratkan setiap nilai, lalu memeriksa apakah jumlahnya 165. Kalkulasi ini hanya menggunakan nilai yang dibuat di memori. Tujuannya adalah memperlihatkan masa hidup iterator, buffering, dan penyelesaian dalam satu program kecil.
Apa yang sebenarnya dimulai ulang ketika nilai berulang?
Swift memisahkan urutan asinkron dari iteratornya. Urutan menyediakan iterator melalui makeAsyncIterator(), lalu iterator bergerak maju melalui pemanggilan next(). Setiap pemanggilan mengembalikan elemen atau nil ketika iterasi telah berakhir. Apple mendokumentasikan kontrak ini di AsyncIteratorProtocol.
Ada dua operasi berbeda yang perlu diperiksa ketika aplikasi berulang kali menerima nilai pertama: membuat iterator lain dan membuat stream lain. Keduanya tidak dapat dipertukarkan. Dalam implementasi pustaka standar saat ini, iterator AsyncStream menyimpan konteks bersama stream, dan makeAsyncIterator() meneruskan konteks yang sudah ada itu ke iterator. Karena itu, membuat iterator lain pada stream yang sama tidak membuat pemutaran ulang independen dari awal. Lihat implementasi resmi AsyncStream.
Sebaliknya, memanggil factory yang membuat stream baru dan mengeluarkan 4, 7, dan 10 akan membuat sumber lain yang berisi nilai-nilai tersebut. Jika setiap pembacaan memanggil factory itu dan hanya mengonsumsi satu elemen, setiap pembacaan dapat menerima 4. Properti komputasi yang membuat stream di dalam getter-nya dapat menghasilkan efek yang sama. Untuk tugas ini, buat sumber sekali dan pertahankan satu iterator untuk seluruh kalkulasi.
Gunakan pembedaan ini saat memutuskan di mana kode perlu dipindahkan: pindahkan pembuatan stream ke luar operasi berulang untuk mempertahankan sumber, dan pindahkan pembuatan iterator ke luar operasi tersebut agar jalur konsumsinya tetap jelas. Jangan mengandalkan pembuatan iterator berulang sebagai mekanisme untuk memulai ulang atau mempertahankan progres.
Jalankan kalkulasi lokal lengkap di Mac Anda
Gunakan Mac dengan toolchain Swift 6 atau yang lebih baru. Di Terminal, periksa kompiler yang terpasang, lalu buat paket executable kecil di direktori baru. Panduan Build a Command-line Tool dari Swift.org mendokumentasikan pemeriksaan versi, pembuatan paket, dan alur kerja swift run.
swift --version
mkdir IteratorDemo
cd IteratorDemo
swift package init --name IteratorDemo --type executable
Temukan sumber executable yang dihasilkan di dalam Sources. Ganti isinya dengan program lengkap di bawah. Namai berkas sumber itu IteratorDemo.swift; jika templat membuat main.swift, ganti namanya dan hapus isi aslinya. Gunakan hanya satu titik masuk executable. Swift.org mendokumentasikan bahwa titik masuk main.swift dan titik masuk @main tidak dapat digabungkan dalam executable yang sama.
Contoh ini tidak memerlukan impor: AsyncStream termasuk dalam Swift, sedangkan Foundation adalah framework terpisah. Kalkulasi ini tidak memerlukan API Foundation. Batasan ini dijelaskan dalam dokumentasi AsyncStream dan ikhtisar Foundation dari Apple.
@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)")
}
}
Jalankan executable dari direktori paket:
swift run IteratorDemo
Keluaran aplikasi yang diharapkan, setelah pesan build apa pun, adalah:
Read 1: 4
Read 2: 7
Read 3: 10
Square total: 165
Finished: true
Aritmetikanya secara eksplisit hanya sebagai ilustrasi: 4 × 4 + 7 × 7 + 10 × 10 = 165. Assertion memeriksa nilai yang tepat, urutannya, kalkulasi, dan perilaku terminal. Kompiler Swift tidak tersedia di lingkungan riset, jadi program ini telah diperiksa tetapi tidak dijalankan di sini.
Mengapa iterator ini berlanjut dengan benar?
Program memanggil makeValues() sekali dan makeAsyncIterator() sekali. Ketiga pembacaannya menggunakan variabel mutable yang sama. Apple mendeklarasikan metode next() milik iterator sebagai mutating dan asinkron, yang menjelaskan penggunaan var iterator dan await iterator.next().
Produser mengeluarkan ketiga nilainya saat stream dibuat, sebelum konsumsi dimulai. Karena itu, kapasitas buffer menjadi bagian dari kontrak contoh ini: .bufferingOldest(3) memiliki ruang untuk ketiga bilangan bulat. Memeriksa hasil setiap yield membuat kegagalan enqueue yang tak terduga terlihat, alih-alih membiarkan kalkulasi berlanjut dengan nilai yang hilang. Apple mendokumentasikan bahwa yield(_:) mencoba melakukan buffer terhadap nilai ketika tidak ada pembacaan yang menunggu.
Memanggil finish() setelah nilai-nilai itu dikeluarkan akan menutup produksi, sambil tetap memungkinkan nilai yang tersimpan dalam buffer untuk dikonsumsi. Proposal Swift yang diterima secara eksplisit menyatakan bahwa elemen dalam buffer sampai ke konsumen sebelum nil terminal. Itulah sebabnya tiga pembacaan pertama berhasil meskipun produser sudah selesai. Lihat AsyncStream dan AsyncThrowingStream, SE-0314.
Pembacaan keempat menemukan stream yang sudah selesai dan habis, lalu mengembalikan nil. Pembacaan kelima mengonfirmasi kontrak terminal: setelah sebuah iterator mengembalikan nil, pembacaan berikutnya juga harus mengembalikan nil. Jangan menafsirkan nil sebagai perintah untuk mencoba lagi nanti atau membuat ulang iterator; AsyncIteratorProtocol mendefinisikannya sebagai akhir iterasi.
Pilih pembacaan eksplisit atau satu loop berkelanjutan
Gunakan pemanggilan next() secara eksplisit ketika operasi yang menaunginya memerlukan kendali di antara setiap pembacaan, seperti pada kalkulasi bernomor ini. Pertahankan iterator dalam cakupan yang mengelola seluruh sesi konsumsi. Helper yang membuat iterator sendiri setiap kali dipanggil mengaburkan kepemilikan itu; helper yang juga membuat ulang stream akan mengubah sumbernya sendiri.
Jika tugasnya hanya memproses setiap elemen sampai selesai, gunakan satu loop for await. Ikhtisar AsyncStream dari Apple menunjukkan bentuk konsumsi yang didukung ini. Untuk program contoh, kode berikut menggantikan seluruh metode main() yang ada; pertahankan metode makeValues() yang sudah ada dan tipe di sekitarnya:
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)")
}
Versi ini menyatakan satu sesi konsumsi berkelanjutan. Pilih versi ini jika tidak diperlukan perintah pembacaan terpisah. Pilih iterator eksplisit jika pembacaan satu per satu merupakan bagian dari struktur tugas. Kedua pendekatan tetap memerlukan sumber yang dibuat dengan benar dan batas akhir yang ditentukan.
Diagnosis pembacaan yang berulang, hilang, atau tidak selesai
Urutan berikut memisahkan masalah masa hidup sumber dari masalah buffering dan penyelesaian. Terapkan langkah-langkah ini pada contoh tetap sebelum menambahkan perilaku lain.
4berulang: periksa pembuatan sumber. Periksa apakah setiap operasi memanggilmakeValues()lagi, atau mengakses getter yang membuat stream baru. Tempatkan pemanggilan factory di luar pembacaan berulang.- Nilai tak terduga di antara konsumen: periksa kepemilikan. Berikan kalkulasi ini satu konsumen dan satu iterator. Beberapa iterator pada stream yang sama tidak menyediakan salinan nilai yang independen.
- Nilai hilang: periksa kapasitas buffer. Contoh ini menghasilkan ketiga bilangan bulat sebelum konsumsi. Buffer yang lebih kecil mengubah nilai mana yang tetap tersedia.
- Pembacaan terakhir yang tidak pernah selesai: periksa penyelesaian. Dalam stream berbasis continuation, buffer yang kosong saja tidak mengakhiri produksi. Pastikan produser berhingga memanggil
finish(). - Pembacaan yang bertumpang tindih: serialkan konsumsi. Tunggu hingga satu pembacaan selesai sebelum memulai pembacaan lain pada iterator.
Perubahan buffer memiliki konsekuensi yang dapat diprediksi dalam kondisi contoh ini. Dengan .bufferingOldest(2), dua nilai pertama tetap tersimpan dan nilai baru 10 dibuang. Dengan .bufferingNewest(2), nilai tertua dalam buffer, 4, dibuang sehingga tersisa 7 dan 10. Ini adalah kesimpulan untuk urutan produksi yang disebutkan, berdasarkan kebijakan bufferingOldest(_:) dan bufferingNewest(_:) yang didokumentasikan Apple.
Kedua kebijakan tersebut tidak membuat produser menunggu konsumsi. Apple mendokumentasikan bahwa yield(_:) segera mengembalikan nilai, jadi mempertahankan satu iterator tidak dapat mengatasi buffer yang terlalu kecil. Untuk kalkulasi tiga elemen berhingga ini, kapasitas tiga sudah cukup; pola produksi yang berbeda memerlukan pertimbangan kapasitas tersendiri.
Akses iterator secara berurutan. Apple menyatakan bahwa AsyncStream.Iterator tidak mematuhi Sendable dan bahwa pemanggilan next() secara bersamaan yang saling bersaing merupakan kesalahan pemrograman. Pembatalan juga memengaruhi continuation: jika tugas konsumen dibatalkan saat menunggu nilai, dokumentasi next() dari Apple menyatakan bahwa stream akan dihentikan. Membuat iterator lain bukan cara yang didokumentasikan untuk melanjutkan stream yang dibatalkan itu.
Untuk kalkulasi lokal ini, kontrak konsumsinya sederhana: buat satu sumber, pertahankan satu iterator mutable, tunggu pembacaan secara berurutan, simpan ketiga nilai dalam buffer, lalu berhenti pada nil. Pilihan tersebut memperjelas progres dan memberi total akhir makna yang dapat diperiksa.
Sumber
- AsyncStream — Apple.
- AsyncIteratorProtocol — Apple.
- AsyncStream.Iterator — Apple.
- next() — Apple.
- yield(_:) — Apple.
- AsyncStream.Continuation.BufferingPolicy.bufferingOldest(_:) — Apple.
- AsyncStream.Continuation.BufferingPolicy.bufferingNewest(_:) — Apple.
- Foundation — Apple.
- Build a Command-line Tool — Swift.org.
- AsyncStream.swift — Proyek Swift.
- AsyncStream and AsyncThrowingStream, SE-0314 — Proyek Swift.