Crea un unico iteratore mutabile e chiama await iterator.next() per ogni valore successivo. Per un piccolo calcolo da riga di comando su Mac, mantieni sia lo stream sia l’iteratore al di fuori dell’operazione di lettura ripetuta e considera nil come indicazione di completamento.

Questa guida consuma i numeri interi sintetici 4, 7 e 10, eleva al quadrato ciascun valore e verifica che il totale sia 165. Il calcolo usa soltanto valori creati in memoria. Lo scopo è rendere visibili la durata di vita dell’iteratore, il buffering e il completamento in un unico piccolo programma.

Che cosa viene effettivamente riavviato quando i valori si ripetono?

Swift distingue una sequenza asincrona dal suo iteratore. La sequenza fornisce un iteratore tramite makeAsyncIterator(), e l’iteratore avanza con le chiamate a next(). Ogni chiamata restituisce un elemento oppure nil quando l’iterazione è terminata. Apple documenta questo contratto in AsyncIteratorProtocol.

Quando un’applicazione riceve ripetutamente il primo valore, occorre esaminare due operazioni diverse: la creazione di un altro iteratore e la creazione di un altro stream. Non sono intercambiabili. Nell’implementazione attuale della libreria standard, un iteratore AsyncStream conserva il contesto condiviso dello stream e makeAsyncIterator() passa questo contesto esistente all’iteratore. Creare un altro iteratore sullo stesso stream, quindi, non avvia una riproduzione indipendente dall’inizio. Consulta l’implementazione ufficiale di AsyncStream.

Al contrario, chiamare una factory che crea un nuovo stream e produce 4, 7 e 10 crea un’altra sorgente che contiene quei valori. Se ogni lettura chiama quella factory e consuma un solo elemento, ogni lettura può ricevere 4. Una proprietà calcolata che crea uno stream nel proprio getter può avere lo stesso effetto. Per questa attività, crea la sorgente una sola volta e conserva un unico iteratore per l’intero calcolo.

Usa questa distinzione per decidere dove spostare il codice: sposta la creazione dello stream fuori dall’operazione ripetuta per preservare la sorgente e sposta la creazione dell’iteratore fuori da essa per mantenere un unico percorso di consumo ben definito. Non affidarti alla creazione ripetuta di iteratori come meccanismo di riavvio o come metodo per mantenere l’avanzamento.

Esegui un calcolo locale completo sul Mac

Usa un Mac con una toolchain Swift 6 o successiva. In Terminal, controlla il compilatore installato, poi crea un piccolo pacchetto eseguibile in una nuova directory. La guida di Swift.org Build a Command-line Tool documenta il controllo della versione, la creazione del pacchetto e il flusso di lavoro di swift run.

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

Trova il sorgente dell’eseguibile generato in Sources. Sostituiscine il contenuto con il programma completo qui sotto. Assegna al file sorgente il nome IteratorDemo.swift; se il modello ha creato main.swift, rinominalo e rimuovi il contenuto originale. Mantieni un solo punto di ingresso eseguibile. Swift.org documenta che un punto di ingresso main.swift e un punto di ingresso @main non possono essere combinati nello stesso eseguibile.

Per questo esempio non servono import: AsyncStream fa parte di Swift, mentre Foundation è un framework separato. Il calcolo non richiede alcuna API di Foundation. Questi confini sono descritti nella documentazione di AsyncStream e nella panoramica di Foundation di 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)")
    }
}

Esegui l’eseguibile dalla directory del pacchetto:

swift run IteratorDemo

L’output atteso dell’applicazione, dopo eventuali messaggi di compilazione, è:

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

L’aritmetica è esplicitamente esemplificativa: 4 × 4 + 7 × 7 + 10 × 10 = 165. Le asserzioni verificano i valori esatti, il loro ordine, il calcolo e il comportamento terminale. Nell’ambiente di ricerca non era disponibile un compilatore Swift, quindi il programma è stato esaminato ma non eseguito.

Perché questo iteratore continua correttamente?

Il programma chiama makeValues() una volta e makeAsyncIterator() una volta. Tutte e tre le letture usano la stessa variabile mutabile. Apple dichiara il metodo next() dell’iteratore come mutante e asincrono, il che spiega sia var iterator sia await iterator.next().

Il producer produce i suoi tre valori durante la creazione dello stream, prima che inizi il consumo. La capacità del buffer fa quindi parte del contratto dell’esempio: .bufferingOldest(3) può contenere tutti e tre i numeri interi. Verificare il risultato di ogni yield rende visibile un eventuale errore inatteso nell’accodamento, invece di lasciare che il calcolo prosegua senza alcuni valori. Apple documenta che yield(_:) tenta di inserire un valore nel buffer quando non è in attesa alcuna lettura.

Chiamare finish() dopo aver prodotto quei valori chiude la produzione e consente comunque di consumare i valori in buffer. La proposta Swift accettata afferma esplicitamente che gli elementi in buffer raggiungono il consumer prima di nil terminale. Ecco perché le prime tre letture riescono anche se il producer ha già terminato. Consulta AsyncStream e AsyncThrowingStream, SE-0314.

La quarta lettura trova lo stream terminato e senza elementi e restituisce nil. La quinta conferma il contratto terminale: dopo che un iteratore ha restituito nil, anche le letture successive devono restituire nil. Non interpretare nil come un invito a riprovare più tardi o a ricreare un iteratore; AsyncIteratorProtocol lo definisce come fine dell’iterazione.

Scegli tra letture esplicite e un unico ciclo continuo

Usa chiamate esplicite a next() quando l’operazione circostante richiede un controllo tra una lettura e l’altra, come nel calcolo numerato. Mantieni l’iteratore nell’ambito che gestisce l’intera sessione di consumo. Un helper che crea il proprio iteratore a ogni invocazione rende poco chiara tale responsabilità; un helper che ricostruisce anche lo stream modifica la sorgente stessa.

Quando l’attività consiste semplicemente nell’elaborare ogni elemento fino al completamento, usa un unico ciclo for await. La panoramica di AsyncStream di Apple mostra questa forma di consumo supportata. Per il programma di esempio, il frammento seguente sostituisce l’intero metodo main() esistente; mantieni il metodo makeValues() esistente e il tipo circostante:

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

Questa versione esprime un’unica sessione di consumo continuo. Sceglila quando non servono comandi di lettura separati. Scegli l’iteratore esplicito quando le singole letture fanno parte della struttura dell’attività. Entrambi gli approcci richiedono comunque una sorgente costruita correttamente e una fine definita.

Individua le cause di letture ripetute, mancanti o non completate

La sequenza seguente separa i problemi di durata di vita della sorgente da quelli di buffering e completamento. Applicala all’esempio fisso prima di aggiungere altri comportamenti.

  1. 4 ripetuto: controlla la creazione della sorgente. Verifica se ogni operazione chiama di nuovo makeValues() o accede a un getter che crea un nuovo stream. Sposta la chiamata alla factory fuori dalle letture ripetute.
  2. Valori inattesi tra consumer: controlla la proprietà. Assegna a questo calcolo un consumer e un iteratore. Più iteratori sullo stesso stream non forniscono copie indipendenti dei suoi valori.
  3. Valori mancanti: controlla la capacità del buffer. L’esempio produce tutti e tre i numeri interi prima del consumo. Un buffer più piccolo cambia quali valori restano disponibili.
  4. Una lettura finale che non termina: controlla il completamento. In uno stream basato su continuation, un buffer vuoto da solo non conclude la produzione. Assicurati che il producer finito chiami finish().
  5. Letture sovrapposte: serializza il consumo. Attendi il completamento di una lettura prima di iniziarne un’altra sull’iteratore.

Con le condizioni di questo esempio, le modifiche al buffer hanno conseguenze prevedibili. Con .bufferingOldest(2), i primi due valori restano e il nuovo valore 10 viene scartato. Con .bufferingNewest(2), il valore più vecchio nel buffer, 4, viene scartato e restano 7 e 10. Sono deduzioni relative all’ordine di produzione indicato, basate sulle politiche documentate da Apple bufferingOldest(_:) e bufferingNewest(_:).

Nessuna delle due politiche fa attendere il producer finché non avviene il consumo. Apple documenta che yield(_:) restituisce immediatamente, quindi mantenere un unico iteratore non compensa un buffer sottodimensionato. Per questo calcolo finito con tre elementi, una capacità di tre è sufficiente; un diverso schema di produzione richiede una valutazione separata della capacità.

Mantieni sequenziale l’accesso all’iteratore. Apple afferma che AsyncStream.Iterator non è conforme a Sendable e che chiamate concorrenti e in conflitto a next() costituiscono un errore del programmatore. Anche la cancellazione influisce sulla continuation: se l’attività di consumo viene annullata mentre attende un valore, la documentazione di next() di Apple afferma che lo stream termina. Creare un altro iteratore non è un modo documentato per riprendere quello stream dopo la cancellazione.

Per questo calcolo locale, il contratto completo di consumo è semplice: crea una sorgente, conserva un iteratore mutabile, attendi le letture in sequenza, mantieni tutti e tre i valori nel buffer e fermati a nil. Queste scelte rendono esplicito l’avanzamento e danno al totale finale un significato verificabile.

Fonti