Un regard approfondi sur les traits pour l’async
Tout au long du chapitre, nous avons utilisé les traits Future, Stream et StreamExt de diverses manières. Jusqu’ici, cependant, nous avons évité d’entrer trop dans les détails de leur fonctionnement ou de la façon dont ils s’articulent ensemble, ce qui convient la plupart du temps pour votre travail Rust quotidien. Parfois, cependant, vous rencontrerez des situations où vous devrez comprendre quelques détails supplémentaires de ces traits, ainsi que le type Pin et le trait Unpin. Dans cette section, nous approfondirons juste assez pour aider dans ces scénarios, en laissant la plongée vraiment profonde à d’autre documentation.
Le trait Future
Commençons par regarder de plus près comment fonctionne le trait Future. Voici comment Rust le définit : rust use std::pin::Pin; use std::task::{Context, Poll}; pub trait Future { type Output; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>; }
#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
}
Cette définition de trait inclut un tas de nouveaux types et aussi une syntaxe que nous n’avons pas vue auparavant, alors parcourons la définition morceau par morceau.
Premièrement, le type associé Output de Future indique en quoi la future se résout. C’est analogue au type associé Item du trait Iterator. Deuxièmement, Future à la méthode poll, qui prend une référence spéciale Pin pour son paramètre self et une référence mutable vers un type Context, et retourné un Poll<Self::Output>. Nous parlerons davantage de Pin et de Context dans un moment. Pour l’instant, concentrons-nous sur ce que la méthode retourné, le type Poll : rust pub enum Poll<T> { Ready(T), Pending, }
#![allow(unused)]
fn main() {
pub enum Poll<T> {
Ready(T),
Pending,
}
}
Ce type Poll est similaire à une Option. Il à une variante qui contient une valeur, Ready(T), et une qui n’en contient pas, Pending. Cependant, Poll signifie quelque chose de très différent d’Option ! La variante Pending indique que la future a encore du travail à faire, donc l’appelant devra vérifier à nouveau plus tard. La variante Ready indique que la Future a terminé son travail et que la valeur T est disponible.
Remarque : il est rare d’avoir besoin d’appeler
polldirectement, mais si vous devez le faire, gardez à l’esprit qu’avec la plupart des futures, l’appelant ne devrait pas appelerpollà nouveau après que la future a retournéReady. Beaucoup de futures paniqueront si elles sont interrogées à nouveau après être devenues prêtes. Les futures qui peuvent être interrogées à nouveau en toute sécurité le diront explicitement dans leur documentation. C’est similaire au comportement deIterator::next.
Quand vous voyez du code qui utilise await, Rust le compilé en coulisses en code qui appelle poll. Si vous revenez à l’encart 17-4, où nous avons affiché le titre de la page pour une seule URL une fois qu’elle s’est résolue, Rust le compilé en quelque chose qui ressemble à peu près (bien que pas exactement) à ceci : rust,ignore match page_title(url).poll() { Ready(page_title) => match page_title { Some(title) => println!("The title for {url} was {title}"), None => println!("{url} had no title"), } Pending => { // But what goes here? } }
match page_title(url).poll() {
Ready(page_title) => match page_title {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
Pending => {
// But what goes here?
}
}
Que devons-nous faire quand la future est encore Pending ? Nous avons besoin d’un moyen de réessayer, encore et encore, jusqu’à ce que la future soit enfin prête. En d’autres termes, nous avons besoin d’une boucle : rust,ignore let mut page_title_fut = page_title(url); loop { match page_title_fut.poll() { Ready(value) => match page_title { Some(title) => println!("The title for {url} was {title}"), None => println!("{url} had no title"), } Pending => { // continue } } }
let mut page_title_fut = page_title(url);
loop {
match page_title_fut.poll() {
Ready(value) => match page_title {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
Pending => {
// continue
}
}
}
Si Rust le compilait exactement en ce code, cependant, chaque await serait bloquant — exactement le contraire de ce que nous recherchions ! À la place, Rust s’assuré que la boucle peut passer le contrôle à quelque chose qui peut mettre en pause le travail sur cette future pour travailler sur d’autres futures puis revérifier celle-ci plus tard. Comme nous l’avons vu, ce quelque chose est un runtime async, et ce travail de planification et de coordination est l’un de ses principaux rôles.
Dans la section « Envoyer des données entre deux tâches en utilisant le passage de messages », nous avons envoyé plusieurs valeurs à travers un canal. Voyons maintenant comment recevoir ces valeurs sous forme de flux (stream) en utilisant async.
Les détails exacts de la façon dont un runtime fait cela dépassent le cadre de ce livre, mais l’essentiel est de voir les mécanismes de base des futures : un runtime interroge chaque future dont il est responsable, remettant la future en sommeil quand elle n’est pas encore prête.
Le type Pin et le trait Unpin
Dans l’encart 17-13, nous avons utilisé la macro trpl::join! pour attendre trois futures. Cependant, il est courant d’avoir une collection comme un vecteur contenant un certain nombre de futures qui ne sera pas connu avant l’exécution. Modifions l’encart 17-13 en le code de l’encart 17-23 qui met les trois futures dans un vecteur et appelle la fonction trpl::join_all à la place, ce qui ne compilera pas encore.
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let tx1 = tx.clone();
let tx1_fut = async move {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx1.send(val).unwrap();
trpl::sleep(Duration::from_secs(1)).await;
}
};
let rx_fut = async {
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
};
let tx_fut = async move {
// --snip--
let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_secs(1)).await;
}
};
let futures: Vec<Box<dyn Future<Output = ()>>> =
vec![Box::new(tx1_fut), Box::new(rx_fut), Box::new(tx_fut)];
trpl::join_all(futures).await;
});
}
Nous mettons chaque future dans un Box pour en faire des objets trait, comme nous l’avons fait dans la section « Retourner les erreurs depuis run » au chapitre 12. (Nous couvrirons les objets trait en détail au chapitre 18.) Utiliser des objets trait nous permet de traiter chacune des futures anonymes produites par ces types comme le même type, car elles implémentent toutes le trait Future.
Cela peut être surprenant. Après tout, aucun des blocs async ne retourné quoi que ce soit, donc chacun produit une Future<Output = ()>. Rappelez-vous cependant que Future est un trait, et que le compilateur crée un enum unique pour chaque bloc async, même quand ils ont des types de sortie identiques. Tout comme vous ne pouvez pas mettre deux structs différentes écrites à la main dans un Vec, vous ne pouvez pas mélanger des enums générés par le compilateur.
Ensuite, nous passons la collection de futures à la fonction trpl::join_all et attendons le résultat. Cependant, cela ne compilé pas ; voici la partie pertinente des messages d’erreur.
error[E0277]: `dyn Future<Output = ()>` cannot be unpinned
--> src/main.rs:48:33
|
48 | trpl::join_all(futures).await;
| ^^^^^ the trait `Unpin` is not implemented for `dyn Future<Output = ()>`
|
= note: consider using the `pin!` macro
consider using `Box::pin` if you need to access the pinned value outside of the current scope
= note: required for `Box<dyn Future<Output = ()>>` to implement `Future`
note: required by a bound in `futures_util::future::join_all::JoinAll`
--> file:///home/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-util-0.3.30/src/future/join_all.rs:29:8
|
27 | pub struct JoinAll<F>
| ------- required by a bound in this struct
28 | where
29 | F: Future,
| ^^^^^^ required by this bound in `JoinAll`
La note dans ce message d’erreur nous dit que nous devrions utiliser la macro pin! pour épingler les valeurs, ce qui signifie les mettre à l’intérieur du type Pin qui garantit que les valeurs ne seront pas déplacées en mémoire. Le message d’erreur dit que l’épinglage est nécessaire car dyn Future<Output = ()> doit implémenter le trait Unpin et ne le fait pas actuellement.
La fonction trpl::join_all retourné une struct appelée JoinAll. Cette struct est générique sur un type F, qui est contraint d’implémenter le trait Future. Attendre directement une future avec await épingle la future implicitement. C’est pourquoi nous n’avons pas besoin d’utiliser pin! partout où nous voulons attendre des futures.
Cependant, nous n’attendons pas directement une future ici. À la place, nous construisons une nouvelle future, JoinAll, en passant une collection de futures à la fonction join_all. La signature de join_all requiert que les types des éléments de la collection implémentent tous le trait Future, et Box<T> n’implémente Future que si le T qu’il encapsule est une future qui implémente le trait Unpin.
C’est beaucoup à absorber ! Pour vraiment comprendre, plongeons un peu plus dans le fonctionnement réel du trait Future, en particulier autour de l’épinglage. Regardez à nouveau la définition du trait Future : rust use std::pin::Pin; use std::task::{Context, Poll}; pub trait Future { type Output; // Required method fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>; }
#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};
pub trait Future {
type Output;
// Required method
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
}
Le paramètre cx et son type Context sont la clé pour comprendre comment un runtime sait réellement quand vérifier une future donnée tout en restant paresseux. Encore une fois, les détails de ce fonctionnement dépassent le cadre de ce chapitre, et vous n’avez généralement besoin d’y penser que lorsque vous écrivez une implémentation personnalisée de Future. Nous nous concentrerons plutôt sur le type de self, car c’est la première fois que nous voyons une méthode où self à une annotation de type. Une annotation de type pour self fonctionne comme les annotations de type pour d’autres paramètres de fonction mais avec deux différences clés :
- Elle indique à Rust quel type
selfdoit avoir pour que la méthode puisse être appelée. - Ce ne peut pas être n’importe quel type. C’est restreint au type sur lequel la méthode est implémentée, une référence ou un pointeur intelligent vers ce type, ou un
Pinencapsulant une référence vers ce type.
Nous en verrons plus sur cette syntaxe au chapitre 18. Pour l’instant, il suffit de savoir que si nous voulons interroger un futur pour vérifier s’il est Pending ou Ready(Output), nous avons besoin d’une référence mutable enveloppée dans Pin vers le type.
Pin est un wrapper pour les types semblables à des pointeurs comme &, &mut, Box et Rc. (Techniquement, Pin fonctionne avec les types qui implémentent les traits Deref ou DerefMut, mais c’est effectivement équivalent à ne travailler qu’avec des références et des pointeurs intelligents.) Pin n’est pas un pointeur lui-même et n’à aucun comportement propre comme Rc et Arc avec le comptage de références ; c’est purement un outil que le compilateur peut utiliser pour imposer des contraintes sur l’utilisation des pointeurs.
Se rappeler que await est implémenté en termes d’appels à poll commence à expliquer le message d’erreur que nous avons vu plus tôt, mais c’était en termes d’Unpin, pas de Pin. Alors comment exactement Pin est-il lié à Unpin, et pourquoi Future a-t-il besoin que self soit dans un type Pin pour appeler poll ?
Rappelez-vous du début de ce chapitre qu’une série de points d’attente dans une future est compilée en une machine à états, et le compilateur s’assuré que cette machine à états respecte toutes les règles normales de Rust concernant la sécurité, y compris l’emprunt et la possession. Pour que cela fonctionne, Rust regarde quelles données sont nécessaires entre un point d’attente et soit le point d’attente suivant, soit la fin du bloc async. Il crée ensuite une variante correspondante dans la machine à états compilée. Chaque variante obtient l’accès dont elle a besoin aux données qui seront utilisées dans cette section du code source, que ce soit en prenant possession de ces données ou en obtenant une référence mutable ou immutable vers celles-ci.
Jusqu’ici, tout va bien : si nous faisons quoi que ce soit de mal avec la possession ou les références dans un bloc async donné, le vérificateur d’emprunts nous le dira. Quand nous voulons déplacer la future qui correspond à ce bloc — comme la déplacer dans un Vec pour la passer à join_all — les choses se compliquent.
Quand nous déplaçons une future — que ce soit en la poussant dans une structure de données pour l’utiliser comme itérateur avec join_all ou en la retournant depuis une fonction — cela signifie en fait déplacer la machine à états que Rust crée pour nous. Et contrairement à la plupart des autres types en Rust, les futures que Rust crée pour les blocs async peuvent se retrouver avec des références vers elles-mêmes dans les champs de n’importe quelle variante donnée, comme montré dans l’illustration simplifiée de la figure 17-4.
Par défaut, cependant, tout objet qui à une référence vers lui-même est dangereux à déplacer, car les références pointent toujours vers l’adresse mémoire réelle de ce qu’elles référencent (voir la figure 17-5). Si vous déplacez la structure de données elle-même, ces références internes continueront de pointer vers l’ancien emplacement. Cependant, cet emplacement mémoire est maintenant invalide. D’une part, sa valeur ne sera pas mise à jour quand vous apporterez des modifications à la structure de données. D’autre part — et c’est plus important — l’ordinateur est maintenant libre de réutiliser cette mémoire à d’autres fins ! Vous pourriez finir par lire des données complètement sans rapport plus tard.
Théoriquement, le compilateur Rust pourrait essayer de mettre à jour chaque référence vers un objet chaque fois qu’il est déplacé, mais cela pourrait ajouter beaucoup de surcoût en performance, surtout si tout un réseau de références doit être mis à jour. Si nous pouvions plutôt nous assurer que la structure de données en question ne se déplace pas en mémoire, nous n’aurions pas besoin de mettre à jour les références. C’est exactement le rôle du vérificateur d’emprunts de Rust : en code sûr, il vous empêche de déplacer tout élément qui à une référence activé vers lui.
Pin s’appuie sur cela pour nous donner exactement la garantie dont nous avons besoin. Quand nous épinglons une valeur en encapsulant un pointeur vers cette valeur dans Pin, elle ne peut plus se déplacer. Ainsi, si vous avez Pin<Box<SomeType>>, vous épinglez en fait la valeur SomeType, pas le pointeur Box. La figure 17-6 illustre ce processus.
En fait, le pointeur Box peut toujours se déplacer librement. Rappelez-vous : ce qui nous importe, c’est de nous assurer que les données finalement référencées restent en place. Si un pointeur se déplace, mais les données vers lesquelles il pointe sont au même endroit, comme dans la figure 17-7, il n’y a pas de problème potentiel. (Comme exercice indépendant, regardez la documentation des types ainsi que le module std::pin et essayez de déterminer comment vous feriez cela avec un Pin encapsulant un Box.) L’essentiel est que le type auto-référentiel lui-même ne peut pas se déplacer, car il est toujours épinglé.
Cependant, la plupart des types sont parfaitement sûrs à déplacer, même s’ils se trouvent derrière un pointeur Pin. Nous n’avons besoin de penser à l’épinglage que quand les éléments ont des références internes. Les valeurs primitives comme les nombres et les booléens sont sûres car elles n’ont évidemment aucune référence interne. La plupart des types avec lesquels vous travaillez normalement en Rust non plus. Vous pouvez déplacer un Vec, par exemple, sans vous inquiéter. Étant donné ce que nous avons vu jusqu’ici, si vous avez un Pin<Vec<String>>, vous devriez tout faire via les API sûres mais restrictives fournies par Pin, même si un Vec<String> est toujours sûr à déplacer s’il n’y a pas d’autres références vers lui. Nous avons besoin d’un moyen de dire au compilateur qu’il est acceptable de déplacer des éléments dans des cas comme celui-ci — et c’est là qu’Unpin entre en jeu.
Unpin est un trait marqueur, similaire aux traits Send et Sync que nous avons vus au chapitre 16, et n’a donc aucune fonctionnalité propre. Les traits marqueurs n’existent que pour dire au compilateur qu’il est sûr d’utiliser le type implémentant un trait donné dans un contexte particulier. Unpin informe le compilateur qu’un type donné n’a pas besoin de respecter de garanties concernant le déplacement sûr de la valeur en question.
Tout comme avec Send et Sync, le compilateur implémente Unpin automatiquement pour tous les types pour lesquels il peut prouver que c’est sûr. Un cas spécial, encore similaire à Send et Sync, est quand Unpin n’est pas implémenté pour un type. La notation pour cela est impl !Unpin for SomeType, où SomeType est le nom d’un type qui doit respecter ces garanties pour être sûr chaque fois qu’un pointeur vers ce type est utilisé dans un Pin.
En d’autres termes, il y a deux choses à garder à l’esprit concernant la relation entre Pin et Unpin. Premièrement, Unpin est le cas « normal », et !Unpin est le cas spécial. Deuxièmement, qu’un type implémente Unpin ou !Unpin n’a d’importance que quand vous utilisez un pointeur épinglé vers ce type comme Pin<&mut SomeType>.
Pour rendre cela concret, pensez à une String : elle à une longueur et les caractères Unicode qui la composent. Nous pouvons encapsuler une String dans Pin, comme vu dans la figure 17-8. Cependant, String implémente automatiquement Unpin, comme la plupart des autres types en Rust.
En conséquence, nous pouvons faire des choses qui seraient illégales si String implémentait !Unpin à la place, comme remplacer une chaîne par une autre exactement au même emplacement en mémoire comme dans la figure 17-9. Cela ne viole pas le contrat de Pin, car String n’a pas de références internes qui rendraient son déplacement dangereux. C’est précisément pourquoi elle implémente Unpin plutôt que !Unpin.
Nous en savons maintenant assez pour comprendre les erreurs signalées pour cet appel à join_all dans l’encart 17-23. Nous avons initialement essayé de déplacer les futures produites par les blocs async dans un Vec<Box<dyn Future<Output = ()>>>, mais comme nous l’avons vu, ces futures peuvent avoir des références internes, donc elles n’implémentent pas automatiquement Unpin. Une fois que nous les épinglons, nous pouvons passer le type Pin résultant dans le Vec, confiants que les données sous-jacentes dans les futures ne seront pas déplacées. L’encart 17-24 montre comment corriger le code en appelant la macro pin! là où chacune des trois futures est définie et en ajustant le type d’objet trait.
extern crate trpl; // required for mdbook test
use std::pin::{Pin, pin};
// --snip--
use std::time::Duration;
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let tx1 = tx.clone();
let tx1_fut = pin!(async move {
// --snip--
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx1.send(val).unwrap();
trpl::sleep(Duration::from_secs(1)).await;
}
});
let rx_fut = pin!(async {
// --snip--
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
});
let tx_fut = pin!(async move {
// --snip--
let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_secs(1)).await;
}
});
let futures: Vec<Pin<&mut dyn Future<Output = ()>>> =
vec![tx1_fut, rx_fut, tx_fut];
trpl::join_all(futures).await;
});
}
Cet exemple compilé et s’exécute maintenant, et nous pourrions ajouter ou retirer des futures du vecteur à l’exécution et les joindre toutes.
Pin et Unpin sont surtout importants pour construire des bibliothèques de bas niveau, ou quand vous construisez un runtime lui-même, plutôt que pour le code Rust quotidien. Quand vous verrez ces traits dans les messages d’erreur, cependant, vous aurez maintenant une meilleure idée de comment corriger votre code !
Remarque : cette combinaison de
PinetUnpinrend possible l’implémentation sûre de toute une classe de types complexes en Rust qui seraient autrement difficiles car ils sont auto-référentiels. Les types qui nécessitentPinapparaissent le plus souvent dans le Rust async aujourd’hui, mais de temps en temps, vous pourriez aussi les voir dans d’autres contextes. Les spécificités du fonctionnement dePinetUnpin, et les règles qu’ils doivent respecter, sont couvertes en détail dans la documentation de l’API pourstd::pin, donc si vous souhaitez en savoir plus, c’est un excellent point de départ. Si vous voulez comprendre comment les choses fonctionnent en coulisses encore plus en détail, voyez les chapitres [2][under-the-hood] et [4][pinning] de [Asynchronous Programming in Rust][async-book].Les détails de fonctionnement de
PinetUnpin, et les règles qu’ils doivent respecter, sont largement couverts dans la documentation de l’API pourstd::pin, donc si vous êtes intéressé par en savoir plus, c’est un excellent point de départ.Si vous voulez comprendre comment les choses fonctionnent sous le capot avec encore plus de détails, consultez les chapitres 2 et 4 de Asynchronous Programming in Rust.
Le trait Stream
Maintenant que vous avez une compréhension plus profonde des traits Future, Pin et Unpin, nous pouvons tourner notre attention vers le trait Stream. Comme vous l’avez appris plus tôt dans le chapitre, les streams sont similaires aux itérateurs asynchrones. Contrairement à Iterator et Future, cependant, Stream n’a pas de définition dans la bibliothèque standard au moment de la rédaction, mais il existe une définition très courante du crate futures utilisée dans tout l’écosystème.
Revoyons les définitions des traits Iterator et Future avant de regarder comment un trait Stream pourrait les fusionner. De Iterator, nous avons l’idée d’une séquence : sa méthode next fournit un Option<Self::Item>. De Future, nous avons l’idée de disponibilité au fil du temps : sa méthode poll fournit un Poll<Self::Output>. Pour représenter une séquence d’éléments qui deviennent disponibles au fil du temps, nous définissons un trait Stream qui rassemble ces fonctionnalités : rust use std::pin::Pin; use std::task::{Context, Poll}; trait Stream { type Item; fn poll_next( self: Pin<&mut Self>, cx: &mut Context<'_> ) -> Poll<Option<Self::Item>>; }
#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};
trait Stream {
type Item;
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>
) -> Poll<Option<Self::Item>>;
}
}
Le trait Stream définit un type associé appelé Item pour le type des éléments produits par le stream. C’est similaire à Iterator, où il peut y avoir de zéro à plusieurs éléments, et contrairement à Future, où il y a toujours un seul Output, même si c’est le type unit ().
Stream définit aussi une méthode pour obtenir ces éléments. Nous l’appelons poll_next, pour rendre clair qu’elle interroge de la même manière que Future::poll et produit une séquence d’éléments de la même manière que Iterator::next. Son type de retour combine Poll avec Option. Le type extérieur est Poll, car il doit être vérifié pour la disponibilité, tout comme une future. Le type intérieur est Option, car il doit signaler s’il y a plus de messages, tout comme un itérateur.
Quelque chose de très similaire à cette définition finira probablement par faire partie de la bibliothèque standard de Rust. En attendant, cela fait partie de la boîte à outils de la plupart des runtimes, donc vous pouvez vous y fier, et tout ce que nous couvrons ensuite devrait généralement s’appliquer !
Dans les exemples que nous avons vus dans la section [« Streams : des futures en séquence »][streams], cependant, nous n’avons pas utilisé poll_next ni Stream, mais plutôt next et StreamExt. Nous pourrions travailler directement avec l’API poll_next en écrivant manuellement nos propres machines à états Stream, bien sûr, tout comme nous pourrions travailler avec les futures directement via leur méthode poll. Utiliser await est beaucoup plus agréable, cependant, et le trait StreamExt fournit la méthode next pour que nous puissions faire exactement cela : rust {{#rustdoc_include ../listings/ch17-async-await/no-listing-stream-ext/src/lib.rs:here}}
#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};
trait Stream {
type Item;
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>>;
}
trait StreamExt: Stream {
async fn next(&mut self) -> Option<Self::Item>
where
Self: Unpin;
// other methods...
}
}
Remarque : la définition réelle que nous avons utilisée plus tôt dans le chapitre est légèrement différente de celle-ci, car elle prend en charge les versions de Rust qui ne supportaient pas encore l’utilisation de fonctions async dans les traits. En conséquence, elle ressemble à ceci :
rust,ignore fn next(&mut self) -> Next<'_, Self> where Self: Unpin;Ce typeNextest unestructqui implémenteFutureet nous permet de nommer la durée de vie de la référence versselfavecNext<'_, Self>, pour queawaitpuisse fonctionner avec cette méthode.fn next(&mut self) -> Next<'_, Self> where Self: Unpin;Ce type
Nextest unestructqui implémenteFutureet nous permet de nommer la durée de vie de la référence àselfavecNext<'_, Self>, afin qu’awaitpuisse fonctionner avec cette méthode.
Le trait StreamExt est aussi le lieu de toutes les méthodes intéressantes disponibles pour utiliser avec les streams. StreamExt est automatiquement implémenté pour chaque type qui implémente Stream, mais ces traits sont définis séparément pour permettre à la communauté d’itérer sur les API de commodité sans affecter le trait fondamental.
Dans la version de StreamExt utilisée dans le crate trpl, le trait non seulement définit la méthode next mais fournit aussi une implémentation par défaut de next qui gère correctement les détails de l’appel à Stream::poll_next. Cela signifie que même quand vous devez écrire votre propre type de données de streaming, vous n’avez qu’à implémenter Stream, et ensuite quiconque utilise votre type de données peut utiliser StreamExt et ses méthodes automatiquement.
C’est tout ce que nous allons couvrir pour les détails de bas niveau de ces traits. Pour conclure, voyons comment les futures (y compris les streams), les tâches et les threads s’articulent ensemble !