Créez un seul itérateur mutable et appelez await iterator.next() pour chaque valeur successive. Pour un petit calcul en ligne de commande sur Mac, gardez le flux et l’itérateur en dehors de l’opération de lecture répétée, et traitez nil comme la fin.
Ce tutoriel consomme les entiers synthétiques 4, 7 et 10, élève chaque valeur au carré et vérifie que le total est 165. Le calcul utilise uniquement des valeurs créées en mémoire. Il vise à rendre visibles, dans un petit programme, la durée de vie de l’itérateur, la mise en mémoire tampon et la fin de l’itération.
Que se passe-t-il réellement lorsque les valeurs se répètent ?
Swift distingue une séquence asynchrone de son itérateur. La séquence fournit un itérateur via makeAsyncIterator(), et l’itérateur avance grâce aux appels à next(). Chaque appel renvoie un élément ou nil lorsque l’itération est terminée. Apple documente ce contrat dans AsyncIteratorProtocol.
Lorsque l’application reçoit plusieurs fois la première valeur, il faut examiner deux opérations distinctes : créer un autre itérateur et créer un autre flux. Elles ne sont pas interchangeables. Dans l’implémentation actuelle de la bibliothèque standard, un itérateur AsyncStream contient le contexte partagé du flux, et makeAsyncIterator() transmet ce contexte existant à l’itérateur. Créer un autre itérateur sur le même flux ne relance donc pas une lecture indépendante depuis le début. Consultez l’implémentation officielle d’AsyncStream.
En revanche, appeler une fabrique qui crée un nouveau flux et émet 4, 7 et 10 crée une autre source contenant ces valeurs. Si chaque lecture appelle cette fabrique et ne consomme qu’un seul élément, chaque lecture peut recevoir 4. Une propriété calculée qui construit un flux dans son accesseur peut produire le même effet. Pour cette tâche, construisez la source une seule fois et conservez un seul itérateur pendant tout le calcul.
Tenez compte de cette distinction pour décider où déplacer le code : déplacez la création du flux en dehors de l’opération répétée pour préserver la source, et déplacez la création de l’itérateur en dehors de cette opération pour conserver un chemin de consommation unique et clair. Ne comptez pas sur la création répétée d’itérateurs pour recommencer la lecture ou pour maintenir la progression.
Exécuter un calcul local complet sur votre Mac
Utilisez un Mac doté de la chaîne d’outils Swift 6 ou ultérieure. Dans Terminal, vérifiez le compilateur installé, puis créez un petit paquet exécutable dans un nouveau répertoire. Le guide de Swift.org Créer un outil en ligne de commande décrit la vérification de version, la création du paquet et le flux de travail swift run.
swift --version
mkdir IteratorDemo
cd IteratorDemo
swift package init --name IteratorDemo --type executable
Repérez le fichier source de l’exécutable généré sous Sources. Remplacez son contenu par le programme complet ci-dessous. Nommez ce fichier source IteratorDemo.swift ; si le modèle a créé main.swift, renommez-le et supprimez son contenu d’origine. Ne conservez qu’un seul point d’entrée exécutable. Swift.org précise qu’un point d’entrée main.swift et un point d’entrée @main ne peuvent pas être combinés dans le même exécutable.
Aucun import n’est nécessaire pour cet exemple : AsyncStream fait partie de Swift, tandis que Foundation est un framework distinct. Le calcul n’utilise aucune API de Foundation. Ces limites sont décrites dans la documentation d’AsyncStream d’Apple et la présentation de 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)")
}
}
Exécutez le programme depuis le répertoire du paquet :
swift run IteratorDemo
Voici la sortie attendue du programme, après les éventuels messages de compilation :
Read 1: 4
Read 2: 7
Read 3: 10
Square total: 165
Finished: true
Le calcul est explicitement donné à titre d’illustration : 4 × 4 + 7 × 7 + 10 × 10 = 165. Les assertions vérifient les valeurs exactes, leur ordre, le calcul et le comportement à la fin. Aucun compilateur Swift n’était disponible dans l’environnement de recherche ; le programme a donc été examiné, mais pas exécuté ici.
Pourquoi cet itérateur continue-t-il correctement ?
Le programme appelle makeValues() une fois et makeAsyncIterator() une fois. Ses trois lectures utilisent toutes la même variable mutable. Apple déclare la méthode next() de l’itérateur comme étant mutante et asynchrone, ce qui explique à la fois var iterator et await iterator.next().
Le producteur émet ses trois valeurs lors de la construction du flux, avant le début de la consommation. La capacité du tampon fait donc partie du contrat de l’exemple : .bufferingOldest(3) peut contenir les trois entiers. Vérifier le résultat de chaque yield permet de détecter tout échec inattendu de mise en file, au lieu de laisser le calcul se poursuivre avec des valeurs manquantes. Apple indique que yield(_:) tente de mettre une valeur en mémoire tampon lorsqu’aucune lecture n’est en attente.
L’appel à finish() après ces émissions clôt la production tout en permettant la consommation des valeurs mises en mémoire tampon. La proposition Swift adoptée précise que les éléments mis en mémoire tampon parviennent au consommateur avant la valeur terminale nil. C’est pourquoi les trois premières lectures réussissent même si le producteur a déjà terminé. Consultez AsyncStream et AsyncThrowingStream, SE-0314.
La quatrième lecture trouve le flux terminé et épuisé, et renvoie nil. La cinquième confirme le contrat de fin : après qu’un itérateur a renvoyé nil, les lectures suivantes doivent aussi renvoyer nil. N’interprétez pas nil comme une instruction de réessayer plus tard ou de reconstruire un itérateur ; AsyncIteratorProtocol le définit comme la fin de l’itération.
Choisir des lectures explicites ou une boucle continue
Utilisez des appels explicites à next() lorsque l’opération englobante doit contrôler chaque lecture, comme dans le calcul numéroté. Gardez l’itérateur dans la portée qui gère toute la session de consommation. Une fonction auxiliaire qui crée son propre itérateur à chaque appel masque cette responsabilité ; une fonction auxiliaire qui reconstruit également le flux modifie la source elle-même.
Lorsque la tâche consiste simplement à traiter chaque élément jusqu’à la fin, utilisez une seule boucle for await. La présentation d’AsyncStream par Apple montre cette forme de consommation prise en charge. Pour le programme d’exemple, le fragment suivant remplace entièrement la méthode main() existante ; conservez la méthode makeValues() existante et le type qui l’entoure :
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)")
}
Cette version exprime une seule session de consommation continue. Choisissez-la si aucune commande de lecture distincte n’est nécessaire. Choisissez l’itérateur explicite si les lectures individuelles font partie de la structure de la tâche. Dans les deux cas, la source doit être correctement construite et sa fin définie.
Diagnostiquer les lectures répétées, manquantes ou inachevées
La séquence suivante permet de distinguer les problèmes de durée de vie de la source de ceux liés à la mise en mémoire tampon et à la fin. Appliquez-la à l’exemple fixe avant d’ajouter d’autres comportements.
4se répète : examinez la création de la source. Vérifiez si chaque opération rappellemakeValues()ou accède à un accesseur qui crée un nouveau flux. Placez l’appel à la fabrique en dehors des lectures répétées.- Valeurs inattendues entre consommateurs : examinez la propriété de la consommation. Attribuez un seul consommateur et un seul itérateur à ce calcul. Plusieurs itérateurs sur un même flux ne fournissent pas de copies indépendantes de ses valeurs.
- Valeurs manquantes : examinez la capacité du tampon. L’exemple produit les trois entiers avant la consommation. Une capacité de tampon inférieure change les valeurs qui restent disponibles.
- Une lecture finale qui ne se termine jamais : examinez la fin de production. Dans un flux basé sur une continuation, un tampon vide ne suffit pas à terminer la production. Assurez-vous que le producteur fini appelle
finish(). - Lectures qui se chevauchent : sérialisez la consommation. Attendez la fin d’une lecture avant d’en commencer une autre sur l’itérateur.
Dans les conditions de cet exemple, les changements de tampon ont des conséquences prévisibles. Avec .bufferingOldest(2), les deux premières valeurs restent et la nouvelle valeur 10 est abandonnée. Avec .bufferingNewest(2), la plus ancienne valeur en tampon, 4, est supprimée ; il reste 7 et 10. Ces conséquences sont déduites de l’ordre de production indiqué et des stratégies de mise en tampon documentées par Apple : bufferingOldest(_:) et bufferingNewest(_:).
Aucune de ces stratégies ne fait attendre le producteur jusqu’à la consommation. Apple indique que yield(_:) renvoie immédiatement ; conserver un seul itérateur ne compense donc pas un tampon trop petit. Pour ce calcul fini à trois éléments, une capacité de trois suffit ; un autre mode de production nécessite une décision de capacité qui lui est propre.
Accédez à l’itérateur de façon séquentielle. Apple précise que AsyncStream.Iterator ne se conforme pas à Sendable et que les appels concurrents conflictuels à next() constituent une erreur de programmation. L’annulation affecte également la continuation : si la tâche consommatrice est annulée pendant l’attente d’une valeur, la documentation de next() d’Apple indique que le flux prend fin. Créer un autre itérateur n’est pas un moyen documenté de reprendre ce flux annulé.
Pour ce calcul local, le contrat de consommation complet est simple : créer une source, conserver un seul itérateur mutable, attendre les lectures de façon séquentielle, préserver les trois valeurs dans le tampon et s’arrêter à nil. Ces choix rendent la progression explicite et donnent au total final un sens vérifiable.
Sources
- AsyncStream — Apple.
- AsyncIteratorProtocol — Apple.
- AsyncStream.Iterator — Apple.
- next() — Apple.
- yield(_:) — Apple.
- AsyncStream.Continuation.BufferingPolicy.bufferingOldest(_:) — Apple.
- AsyncStream.Continuation.BufferingPolicy.bufferingNewest(_:) — Apple.
- Foundation — Apple.
- Créer un outil en ligne de commande — Swift.org.
- AsyncStream.swift — Projet Swift.
- AsyncStream and AsyncThrowingStream, SE-0314 — Projet Swift.