Skapa en muterbar iterator och anropa await iterator.next() för varje efterföljande värde. För en liten beräkning på kommandoraden på Mac håller du både strömmen och iteratorn utanför den upprepade läsoperationen och behandlar nil som avslut.

Den här genomgången förbrukar heltal skapade för exemplet: 4, 7 och 10, kvadrerar varje värde och kontrollerar att summan är 165. Beräkningen använder bara värden som skapats i minnet. Syftet är att göra iteratorns livslängd, buffring och avslut synliga i ett litet program.

Vad är det egentligen som startar om när värdena upprepas?

Swift skiljer mellan en asynkron sekvens och dess iterator. Sekvensen tillhandahåller en iterator genom makeAsyncIterator(), och iteratorn går vidare genom anrop till next(). Varje anrop returnerar ett element eller nil när iterationen är avslutad. Apple dokumenterar detta kontrakt i AsyncIteratorProtocol.

Det finns två olika operationer att granska när ett program upprepade gånger tar emot det första värdet: att skapa en ny iterator och att skapa en ny ström. De är inte utbytbara. I den aktuella standardbiblioteksimplementeringen håller en AsyncStream-iterator strömmens delade kontext, och makeAsyncIterator() skickar den befintliga kontexten vidare till iteratorn. Att skapa ytterligare en iterator för samma ström startar därför inte en oberoende återuppspelning från början. Se den officiella AsyncStream-implementeringen.

Om du däremot anropar en fabrik som skapar en ny ström och skickar ut 4, 7 och 10, skapas ytterligare en källa som innehåller dessa värden. Om varje läsning anropar fabriken och bara förbrukar ett element kan varje läsning få 4. En beräknad egenskap som skapar en ström i sin getter kan få samma effekt. I den här uppgiften skapar du källan en gång och behåller en iterator under hela beräkningen.

Använd den här skillnaden när du avgör var koden ska flyttas: flytta strömskapandet utanför den upprepade operationen för att bevara källan, och flytta iteratorskapandet utanför den för att behålla en tydlig väg för förbrukningen. Lita inte på att upprepad iteratorskapning fungerar vare sig som en omstartsmekanism eller som ett sätt att bevara förloppet.

Kör en fullständig lokal beräkning på din Mac

Använd en Mac med Swift 6 eller en senare verktygskedja. Kontrollera den installerade kompilatorn i Terminal och skapa sedan ett litet körbart paket i en ny katalog. Swift.orgs guide Build a Command-line Tool beskriver versionskontrollen, paketskapandet och arbetsflödet med swift run.

swift --version
mkdir IteratorDemo
cd IteratorDemo
swift package init --name IteratorDemo --type executable

Leta reda på den genererade körbara källfilen under Sources. Ersätt dess innehåll med hela programmet nedan. Döp källfilen till IteratorDemo.swift; om mallen skapade main.swift byter du namn på filen och tar bort dess ursprungliga innehåll. Behåll bara en startpunkt för det körbara programmet. Swift.org dokumenterar att en startpunkt av typen main.swift och en startpunkt av typen @main inte kan kombineras i samma körbara program.

Inga importer behövs i det här exemplet: AsyncStream ingår i Swift, medan Foundation är ett separat ramverk. Beräkningen behöver inget API från Foundation. Dessa gränser beskrivs i Apples AsyncStream-dokumentation och översikt över 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)")
    }
}

Kör det körbara programmet från paketkatalogen:

swift run IteratorDemo

Det förväntade programmets utdata, efter eventuella byggmeddelanden, är:

Read 1: 4
Read 2: 7
Read 3: 10
Square total: 165
Finished: true

Aritmetiken är uttryckligen illustrativ: 4 × 4 + 7 × 7 + 10 × 10 = 165. Påståendena kontrollerar de exakta värdena, deras ordning, beräkningen och det avslutande beteendet. En Swift-kompilator var inte tillgänglig i forskningsmiljön, så programmet granskades men kördes inte här.

Varför fortsätter den här iteratorn som den ska?

Programmet anropar makeValues() en gång och makeAsyncIterator() en gång. Alla tre läsningar använder samma muterbara variabel. Apple deklarerar iteratorns next()-metod som muterande och asynkron, vilket förklarar både var iterator och await iterator.next().

Producenten skickar ut sina tre värden när strömmen skapas, innan förbrukningen börjar. Buffertkapaciteten är därför en del av exemplet: .bufferingOldest(3) rymmer alla tre heltal. Genom att kontrollera resultatet av varje yield blir oväntade köfel synliga i stället för att beräkningen fortsätter med saknade värden. Apple dokumenterar att yield(_:) försöker buffra ett värde när ingen läsning väntar.

Om du anropar finish() efter dessa utskick stängs produktionen, samtidigt som buffrade värden fortfarande kan förbrukas. Det godkända Swift-förslaget anger uttryckligen att buffrade element når konsumenten före det avslutande nil. Därför lyckas de tre första läsningarna även om producenten redan har avslutat. Se AsyncStream och AsyncThrowingStream, SE-0314.

Den fjärde läsningen når den avslutade och uttömda strömmen och returnerar nil. Den femte bekräftar avslutskontraktet: när en iterator har returnerat nil måste efterföljande läsningar också returnera nil. Tolka inte nil som en instruktion att försöka igen senare eller skapa en ny iterator; AsyncIteratorProtocol definierar det som slutet på iterationen.

Välj mellan explicita läsningar och en enda sammanhängande loop

Använd explicita anrop till next() när den omgivande operationen behöver kunna styra vad som händer mellan enskilda läsningar, som i den numrerade beräkningen. Behåll iteratorn i det scope som äger hela förbrukningssessionen. En hjälpfunktion som skapar en egen iterator vid varje anrop gör ägarskapet otydligt; en hjälpfunktion som också återskapar strömmen ändrar själva källan.

När uppgiften bara är att bearbeta varje element fram till avslut använder du en enda for await-loop. Apples översikt över AsyncStream visar den här förbrukningsformen. För exempelprogrammet ersätter följande kodfragment hela den befintliga metoden main(); behåll den befintliga metoden makeValues() och den omgivande typen:

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)")
}

Den här versionen beskriver en enda sammanhängande förbrukningssession. Välj den när separata läskommandon inte behövs. Välj den explicita iteratorn när enskilda läsningar ingår i uppgiftens struktur. Båda tillvägagångssätten kräver fortfarande en korrekt skapad källa och ett definierat slut.

Felsök upprepade, saknade eller ofullbordade läsningar

Följande ordningsföljd skiljer problem med källans livslängd från problem med buffring och avslut. Tillämpa den på det fasta exemplet innan du lägger till mer funktionalitet.

  1. 4 upprepas: granska källans skapande. Kontrollera om varje operation anropar makeValues() igen, eller läser en getter som skapar en ny ström. Placera fabriksanropet utanför de upprepade läsningarna.
  2. Oväntade värden mellan konsumenter: granska ägarskapet. Ge den här beräkningen en konsument och en iterator. Flera iteratorer för samma ström ger inte oberoende kopior av dess värden.
  3. Värden saknas: granska buffertkapaciteten. Exemplet producerar alla tre heltal innan förbrukningen börjar. En mindre buffert ändrar vilka värden som finns kvar.
  4. En sista läsning som aldrig avslutas: granska avslutet. I en fortsättningsbaserad ström avslutar en tom buffert inte produktionen. Se till att den ändliga producenten anropar finish().
  5. Överlappande läsningar: gör förbrukningen sekventiell. Vänta på en läsning innan du påbörjar nästa för iteratorn.

Buffertändringar får förutsägbara följder under förhållandena i det här exemplet. Med .bufferingOldest(2) blir de två första värdena kvar och det nya värdet 10 kastas bort. Med .bufferingNewest(2) kastas det äldsta buffrade värdet 4 bort, så 7 och 10 blir kvar. Detta följer av den angivna produktionsordningen och Apples dokumenterade policyer bufferingOldest(_:) och bufferingNewest(_:).

Ingen av policyerna får producenten att vänta på förbrukningen. Apple dokumenterar att yield(_:) returnerar omedelbart, så en bibehållen iterator kan inte kompensera för en för liten buffert. För den här ändliga beräkningen med tre element räcker kapaciteten tre; ett annat produktionsmönster kräver ett eget beslut om kapacitet.

Håll åtkomsten till iteratorn sekventiell. Apple anger att AsyncStream.Iterator inte överensstämmer med Sendable och att samtidiga, konkurrerande anrop till next() är ett programmerarfel. Avbrytning påverkar också fortsättningen: om den konsumerande uppgiften avbryts medan den väntar på ett värde, avslutas strömmen enligt Apples dokumentation för next(). Att skapa en ny iterator är inte ett dokumenterat sätt att återuppta den avbrutna strömmen.

För den här lokala beräkningen är hela förbrukningskontraktet enkelt: skapa en källa, behåll en muterbar iterator, invänta läsningarna i tur och ordning, bevara alla tre värden i bufferten och stoppa vid nil. De valen gör förloppet tydligt och ger slutsumman en kontrollerbar betydelse.

Källor