Utwórz jeden modyfikowalny iterator i wywołuj await iterator.next() dla każdej kolejnej wartości. W przypadku niewielkiego obliczenia w wierszu poleceń na Macu zachowaj strumień i iterator poza powtarzaną operacją odczytu, a nil traktuj jako koniec.

W tym omówieniu przetwarzane są syntetyczne liczby całkowite 4, 7 i 10; każda z nich jest podnoszona do kwadratu, a następnie sprawdzane jest, czy suma wynosi 165. Obliczenie wykorzystuje wyłącznie wartości utworzone w pamięci. Ma ono pokazać w jednym małym programie czas życia iteratora, buforowanie i zakończenie.

Co właściwie uruchamia się ponownie, gdy wartości się powtarzają?

Swift rozdziela sekwencję asynchroniczną od jej iteratora. Sekwencja udostępnia iterator za pomocą makeAsyncIterator(), a iterator przechodzi do kolejnych elementów przez wywołania next(). Każde wywołanie zwraca element albo nil, gdy iteracja dobiegnie końca. Apple opisuje ten kontrakt w AsyncIteratorProtocol.

Gdy aplikacja wielokrotnie otrzymuje pierwszą wartość, należy sprawdzić dwie różne operacje: utworzenie kolejnego iteratora i utworzenie kolejnego strumienia. Nie są one równoważne. W bieżącej implementacji biblioteki standardowej iterator AsyncStream przechowuje współdzielony kontekst strumienia, a makeAsyncIterator() przekazuje mu ten istniejący kontekst. Utworzenie kolejnego iteratora dla tego samego strumienia nie rozpoczyna więc niezależnego odtwarzania od początku. Zobacz oficjalną implementację AsyncStream.

Natomiast wywołanie funkcji tworzącej nowy strumień i emitującej 4, 7 i 10 tworzy kolejne źródło zawierające te wartości. Jeśli każdy odczyt wywołuje tę funkcję i pobiera tylko jeden element, każdy odczyt może zwrócić 4. Ten sam efekt może powodować właściwość obliczeniowa, której getter tworzy strumień. Na potrzeby tego zadania utwórz źródło tylko raz i zachowaj jeden iterator przez całe obliczenie.

Pamiętaj o tym rozróżnieniu, decydując, gdzie przenieść kod: przenieś tworzenie strumienia poza powtarzaną operację, aby zachować źródło, a tworzenie iteratora przenieś poza nią, aby zachować jedną przejrzystą ścieżkę pobierania elementów. Nie polegaj na ponownym tworzeniu iteratora jako sposobie na rozpoczęcie od początku ani na zachowanie postępu.

Wykonaj kompletne lokalne obliczenie na Macu

Użyj Maca z zestawem narzędzi Swift 6 lub nowszym. W Terminal sprawdź zainstalowany kompilator, a następnie utwórz mały pakiet pliku wykonywalnego w nowym katalogu. Przewodnik Swift.org Tworzenie narzędzia wiersza poleceń opisuje sprawdzanie wersji, tworzenie pakietu i przepływ pracy z swift run.

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

Znajdź wygenerowany plik źródłowy programu wykonywalnego w katalogu Sources. Zastąp jego zawartość kompletnym programem poniżej. Nazwij ten plik źródłowy IteratorDemo.swift; jeśli szablon utworzył main.swift, zmień jego nazwę i usuń pierwotną zawartość. Pozostaw tylko jeden punkt wejścia programu wykonywalnego. Swift.org wyjaśnia, że punktu wejścia main.swift i punktu wejścia @main nie można łączyć w tym samym programie wykonywalnym.

W tym przykładzie nie są potrzebne żadne importy: AsyncStream należy do Swifta, natomiast Foundation jest osobnym frameworkiem. Obliczenie nie wymaga żadnego API Foundation. Te granice opisano w dokumentacji Apple: AsyncStream i przeglądzie 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)")
    }
}

Uruchom program wykonywalny z katalogu pakietu:

swift run IteratorDemo

Oczekiwany wynik działania programu, po ewentualnych komunikatach kompilacji, to:

Odczyt 1: 4
Odczyt 2: 7
Odczyt 3: 10
Suma kwadratów: 165
Zakończono: true

Działanie arytmetyczne jest wyraźnie podane jako przykład: 4 × 4 + 7 × 7 + 10 × 10 = 165. Asercje sprawdzają dokładne wartości, ich kolejność, obliczenie i zachowanie po zakończeniu. W środowisku badawczym kompilator Swifta był niedostępny, dlatego program został przeanalizowany, ale nie uruchomiony.

Dlaczego ten iterator działa poprawnie?

Program wywołuje makeValues() raz i makeAsyncIterator() raz. Wszystkie trzy odczyty korzystają z tej samej zmiennej modyfikowalnej. Apple deklaruje metodę next() iteratora jako mutującą i asynchroniczną, co wyjaśnia zarówno użycie var iterator, jak i await iterator.next().

Producent emituje trzy wartości podczas tworzenia strumienia, zanim rozpocznie się ich pobieranie. Pojemność bufora jest więc częścią kontraktu tego przykładu: .bufferingOldest(3) mieści wszystkie trzy liczby całkowite. Sprawdzanie wyniku każdego yield pozwala wykryć nieoczekiwane niepowodzenie dodania do kolejki, zamiast kontynuować obliczenie z brakującymi wartościami. Apple dokumentuje, że yield(_:) próbuje buforować wartość, gdy nie oczekuje na nią żaden odczyt.

Wywołanie finish() po emisji tych wartości zamyka produkcję, ale umożliwia pobranie wartości z bufora. Zatwierdzony projekt Swifta wyraźnie stwierdza, że zbuforowane elementy docierają do odbiorcy przed końcowym nil. Dlatego pierwsze trzy odczyty kończą się powodzeniem, mimo że producent zakończył już działanie. Zobacz AsyncStream i AsyncThrowingStream, SE-0314.

Czwarty odczyt napotyka zakończony i opróżniony strumień, więc zwraca nil. Piąty potwierdza kontrakt końcowy: gdy iterator zwrócił już nil, kolejne odczyty również muszą zwracać nil. Nie traktuj nil jako polecenia, by spróbować ponownie później lub utworzyć nowy iterator; AsyncIteratorProtocol definiuje tę wartość jako koniec iteracji.

Wybierz jawne odczyty albo jedną ciągłą pętlę

Używaj jawnych wywołań next(), gdy otaczająca operacja wymaga kontroli między poszczególnymi odczytami, jak w ponumerowanym obliczeniu. Zachowaj iterator w zakresie, który odpowiada za całą sesję pobierania elementów. Funkcja pomocnicza, która przy każdym wywołaniu tworzy własny iterator, zaciemnia tę odpowiedzialność; funkcja, która dodatkowo odtwarza strumień, zmienia samo źródło.

Jeśli zadanie polega po prostu na przetworzeniu każdego elementu aż do końca, użyj jednej pętli for await. Przegląd AsyncStream firmy Apple przedstawia tę obsługiwaną formę pobierania elementów. W przykładowym programie poniższy fragment zastępuje całą istniejącą metodę main(); pozostaw istniejącą metodę makeValues() i otaczający ją typ:

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

Ta wersja przedstawia pojedynczą, ciągłą sesję pobierania elementów. Wybierz ją, gdy osobne polecenia odczytu nie są potrzebne. Wybierz jawny iterator, gdy poszczególne odczyty są częścią struktury zadania. Oba podejścia wymagają poprawnie utworzonego źródła i określonego punktu końcowego.

Diagnozowanie powtarzających się, brakujących lub niedokończonych odczytów

Poniższa kolejność pozwala oddzielić problemy z czasem życia źródła od problemów z buforowaniem i zakończeniem. Zastosuj ją do poprawionego przykładu, zanim dodasz kolejne zachowania.

  1. Powtarzające się 4: sprawdź tworzenie źródła. Sprawdź, czy każda operacja ponownie wywołuje makeValues() albo odwołuje się do gettera tworzącego nowy strumień. Przenieś wywołanie funkcji tworzącej strumień poza powtarzane odczyty.
  2. Nieoczekiwane wartości u różnych odbiorców: sprawdź własność. Przydziel temu obliczeniu jednego odbiorcę i jeden iterator. Kilka iteratorów tego samego strumienia nie zapewnia niezależnych kopii jego wartości.
  3. Brakujące wartości: sprawdź pojemność bufora. W przykładzie wszystkie trzy liczby całkowite są produkowane przed rozpoczęciem pobierania. Mniejszy bufor zmienia to, które wartości pozostają dostępne.
  4. Ostatni odczyt, który nigdy się nie kończy: sprawdź zakończenie. W strumieniu opartym na kontynuacji sam pusty bufor nie kończy produkcji. Upewnij się, że skończony producent wywołuje finish().
  5. Nakładające się odczyty: wykonuj pobieranie sekwencyjnie. Zaczekaj na zakończenie jednego odczytu, zanim rozpoczniesz kolejny na iteratorze.

Zmiany bufora mają przewidywalne skutki przy założeniach tego przykładu. Przy .bufferingOldest(2) pozostają dwie pierwsze wartości, a nowa wartość 10 zostaje odrzucona. Przy .bufferingNewest(2) odrzucana jest najstarsza zbuforowana wartość 4, a pozostają 7 i 10. Są to wnioski wynikające z podanej kolejności produkcji i udokumentowanych przez Apple zasad bufferingOldest(_:) oraz bufferingNewest(_:).

Żadna z tych zasad nie powoduje, że producent czeka na pobranie elementów. Apple dokumentuje, że yield(_:) zwraca wynik natychmiast, więc zachowanie jednego iteratora nie zrekompensuje zbyt małego bufora. W tym skończonym obliczeniu z trzema elementami wystarczy pojemność trzech; inny wzorzec produkcji wymaga osobnej decyzji dotyczącej pojemności.

Dostęp do iteratora zachowuj sekwencyjnie. Apple stwierdza, że AsyncStream.Iterator nie jest zgodny z Sendable, a równoczesne wywołania next(), które ze sobą konkurują, są błędem programisty. Anulowanie wpływa również na kontynuację: jeśli zadanie pobierające zostanie anulowane podczas oczekiwania na wartość, dokumentacja Apple dotycząca next() informuje, że strumień zostaje zakończony. Utworzenie kolejnego iteratora nie jest udokumentowanym sposobem wznowienia tego anulowanego strumienia.

W tym lokalnym obliczeniu kompletny kontrakt pobierania elementów jest prosty: utwórz jedno źródło, zachowaj jeden modyfikowalny iterator, czekaj na odczyty wykonywane sekwencyjnie, zachowaj wszystkie trzy wartości w buforze i zakończ na nil. Dzięki tym wyborom postęp jest jawny, a końcowa suma ma weryfikowalne znaczenie.

Źródła