Maak één veranderbare iterator en roep voor elke volgende waarde await iterator.next() aan. Houd voor een kleine Mac-berekening via de opdrachtregel zowel de stream als de iterator buiten de herhaalde leesbewerking, en behandel nil als voltooiing.

Deze walkthrough verwerkt de synthetische gehele getallen 4, 7 en 10, kwadrateert elke waarde en controleert of de som 165 is. De berekening gebruikt alleen waarden die in het geheugen zijn aangemaakt. Het doel is om de levensduur van de iterator, buffering en voltooiing zichtbaar te maken in één klein programma.

Wat wordt er eigenlijk opnieuw gestart als waarden zich herhalen?

Swift scheidt een asynchrone sequentie van de bijbehorende iterator. De sequentie levert een iterator via makeAsyncIterator(), en de iterator gaat verder met aanroepen van next(). Elke aanroep retourneert een element of nil wanneer de iteratie is beëindigd. Apple beschrijft dit contract in AsyncIteratorProtocol.

Er zijn twee verschillende bewerkingen die je moet onderzoeken wanneer een toepassing herhaaldelijk de eerste waarde ontvangt: een nieuwe iterator maken en een nieuwe stream maken. Die zijn niet uitwisselbaar. In de huidige implementatie van de standaardbibliotheek bevat een AsyncStream-iterator de gedeelde context van de stream, en makeAsyncIterator() geeft die bestaande context door aan de iterator. Een andere iterator op dezelfde stream maken, begint dus geen onafhankelijke herhaling vanaf het begin. Zie de officiële AsyncStream-implementatie.

Een factory aanroepen die daarentegen een nieuwe stream maakt en 4, 7 en 10 oplevert, maakt een nieuwe bron met die waarden. Als elke read die factory aanroept en slechts één element verwerkt, kan elke read 4 ontvangen. Een berekende eigenschap die in zijn getter een stream maakt, kan hetzelfde effect hebben. Maak voor deze taak de bron één keer aan en gebruik gedurende de hele berekening één iterator.

Gebruik dit onderscheid om te bepalen waar je code naartoe verplaatst: verplaats het maken van de stream buiten de herhaalde bewerking om de bron te behouden, en verplaats het maken van de iterator daarbuiten om één duidelijk verwerkingspad te behouden. Vertrouw niet op het herhaald aanmaken van iterators als manier om opnieuw te beginnen of om voortgang vast te houden.

Voer een volledige lokale berekening uit op je Mac

Gebruik een Mac met Swift 6 of een nieuwere toolchain. Controleer in Terminal welke compiler is geïnstalleerd en maak vervolgens in een nieuwe map een klein uitvoerbaar pakket. De handleiding Build a Command-line Tool van Swift.org beschrijft het controleren van de versie, het aanmaken van een pakket en de workflow met swift run.

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

Zoek de gegenereerde broncode voor het uitvoerbare bestand in Sources. Vervang de inhoud door het volledige onderstaande programma. Noem het bronbestand IteratorDemo.swift; als de sjabloon main.swift heeft aangemaakt, hernoem dat bestand dan en verwijder de oorspronkelijke inhoud. Gebruik slechts één startpunt voor het uitvoerbare bestand. Swift.org beschrijft dat een startpunt met main.swift en een startpunt met @main niet in hetzelfde uitvoerbare bestand gecombineerd kunnen worden.

Voor dit voorbeeld zijn geen imports nodig: AsyncStream hoort bij Swift, terwijl Foundation een apart framework is. De berekening heeft geen Foundation-API nodig. Deze grenzen worden toegelicht in Apples AsyncStream-documentatie en het Foundation-overzicht.

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

Voer het uitvoerbare bestand uit vanuit de pakketmap:

swift run IteratorDemo

De verwachte uitvoer van de toepassing, na eventuele buildmeldingen, is:

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

De rekenkundige bewerking is nadrukkelijk illustratief: 4 × 4 + 7 × 7 + 10 × 10 = 165. De assertions controleren de exacte waarden, hun volgorde, de berekening en het terminale gedrag. In de onderzoeksomgeving was geen Swift-compiler beschikbaar, dus het programma is hier nagekeken maar niet uitgevoerd.

Waarom gaat deze iterator correct verder?

Het programma roept makeValues() één keer en makeAsyncIterator() één keer aan. Voor alle drie reads wordt dezelfde veranderbare variabele gebruikt. Apple declareert de methode next() van de iterator als muterend en asynchroon, wat zowel var iterator als await iterator.next() verklaart.

De producent levert de drie waarden tijdens het aanmaken van de stream, voordat het verwerken begint. De buffercapaciteit maakt daarom deel uit van het contract van dit voorbeeld: .bufferingOldest(3) biedt ruimte voor alle drie gehele getallen. Door het resultaat van elke yield te controleren, wordt een onverwachte fout bij het toevoegen aan de wachtrij zichtbaar, in plaats van dat de berekening doorgaat met ontbrekende waarden. Apple beschrijft dat yield(_:) een waarde probeert te bufferen wanneer er geen read wacht.

finish() aanroepen na die yields beëindigt de productie, terwijl gebufferde waarden nog verwerkt kunnen worden. Het geaccepteerde Swift-voorstel stelt expliciet dat gebufferde elementen de consumer bereiken voordat de terminale nil wordt geretourneerd. Daarom slagen de eerste drie reads, ook al is de producent al klaar. Zie AsyncStream and AsyncThrowingStream, SE-0314.

De vierde read komt bij de beëindigde, lege stream en retourneert nil. De vijfde bevestigt het terminale contract: nadat een iterator nil heeft geretourneerd, moeten volgende reads ook nil retourneren. Interpreteer nil niet als een opdracht om het later opnieuw te proberen of een iterator opnieuw aan te maken; AsyncIteratorProtocol definieert dit als het einde van de iteratie.

Kies voor expliciete reads of één doorlopende lus

Gebruik expliciete aanroepen van next() wanneer de omringende bewerking controle nodig heeft tussen afzonderlijke reads, zoals bij de genummerde berekening. Houd de iterator binnen de scope die verantwoordelijk is voor de volledige verwerkingssessie. Een helper die bij elke aanroep zelf een iterator maakt, maakt dat eigenaarschap onduidelijk; een helper die de stream ook opnieuw aanmaakt, verandert de bron zelf.

Als de taak alleen bestaat uit het verwerken van elk element tot de voltooiing, gebruik dan één for await-lus. Apples AsyncStream-overzicht laat deze ondersteunde verwerkingsvorm zien. Voor het voorbeeldprogramma vervangt het volgende fragment de volledige bestaande methode main(); behoud de bestaande methode makeValues() en het omliggende type:

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

Deze versie beschrijft één doorlopende verwerkingssessie. Kies deze aanpak wanneer er geen afzonderlijke leesopdrachten nodig zijn. Kies de expliciete iterator wanneer afzonderlijke reads deel uitmaken van de structuur van de taak. Beide aanpakken vereisen nog steeds een correct aangemaakte bron en een vastgesteld einde.

Spoor herhaalde, ontbrekende of onafgemaakte reads op

De volgende reeks stappen helpt problemen met de levensduur van de bron te onderscheiden van problemen met buffering en voltooiing. Pas deze eerst toe op het vaste voorbeeld voordat je gedrag toevoegt.

  1. 4 wordt herhaald: controleer hoe de bron wordt aangemaakt. Kijk of elke bewerking opnieuw makeValues() aanroept, of een getter gebruikt die een nieuwe stream maakt. Houd de factory-aanroep buiten de herhaalde reads.
  2. Onverwachte waarden bij meerdere consumers: controleer het eigenaarschap. Geef deze berekening één consumer en één iterator. Meerdere iterators op dezelfde stream leveren geen onafhankelijke kopieën van de waarden.
  3. Ontbrekende waarden: controleer de buffercapaciteit. Het voorbeeld produceert alle drie gehele getallen voordat het verwerken begint. Een kleinere buffer verandert welke waarden beschikbaar blijven.
  4. Een laatste read die nooit voltooid wordt: controleer de voltooiing. Bij een stream op basis van een continuation beëindigt een lege buffer de productie op zichzelf niet. Zorg ervoor dat de eindige producent finish() aanroept.
  5. Overlappende reads: verwerk ze na elkaar. Wacht op één read voordat je met een andere begint op de iterator.

Onder de omstandigheden van dit voorbeeld hebben bufferwijzigingen voorspelbare gevolgen. Met .bufferingOldest(2) blijven de eerste twee waarden behouden en wordt de nieuwe waarde 10 weggelaten. Met .bufferingNewest(2) wordt de oudste gebufferde waarde 4 verwijderd, zodat 7 en 10 overblijven. Dit zijn gevolgtrekkingen voor de beschreven productievolgorde op basis van Apples gedocumenteerde beleidsregels bufferingOldest(_:) en bufferingNewest(_:).

Geen van beide beleidsregels laat de producent wachten tot er wordt gelezen. Apple documenteert dat yield(_:) onmiddellijk terugkeert. Eén iterator behouden kan dus niet compenseren voor een te kleine buffer. Voor deze eindige berekening met drie elementen volstaat een capaciteit van drie; een ander productiepatroon vereist een eigen capaciteitskeuze.

Houd de toegang tot de iterator sequentieel. Apple stelt dat AsyncStream.Iterator niet voldoet aan Sendable en dat gelijktijdige, conflicterende aanroepen van next() een programmeerfout zijn. Annulering heeft ook gevolgen voor de continuation: als de consumerende taak wordt geannuleerd terwijl die op een waarde wacht, beëindigt de stream volgens Apples documentatie over next(). Een andere iterator aanmaken is geen gedocumenteerde manier om die geannuleerde stream te hervatten.

Voor deze lokale berekening is het volledige verwerkingscontract klein: maak één bron aan, behoud één veranderbare iterator, wacht na elkaar op reads, behoud alle drie waarden in de buffer en stop bij nil. Zo wordt de voortgang expliciet en krijgt het eindtotaal een controleerbare betekenis.

Bronnen