Pourquoi retourner un itérateur est un casse-tête
\n\nAvez-vous déjà écrit une fonction en Rust qui devait retourner une collection traitée via une chaîne d'itérateurs ? Si oui, vous avez sûrement été confronté à la question : "Quel type retourner ?" Il semblerait logique d'écrire fn get_data() -> Iterator, mais le compilateur renvoie immédiatement une erreur : "the trait `Iterator` cannot be made into an object".
Ce problème est si courant que sur Stack Overflow, la question "What is the correct way to return an Iterator (or any other trait)?" a recueilli des milliers de vues. Et ce n'est pas surprenant — le bon choix influence non seulement la commodité du code, mais aussi les performances. Dans cet article, nous analyserons toutes les façons de retourner des itérateurs en Rust, leurs avantages et inconvénients, et surtout comment ne pas perdre en performance pour rien.
\n\nCet article est écrit pour pythonlib.ru — un site éducatif où nous n'avons pas peur de regarder sous le capot des langages populaires. Rust, avec ses abstractions à coût zéro, nous offre une opportunité unique d'écrire un code à la fois élégant et rapide. Mais seulement si nous savons utiliser correctement ses outils.
\n\nLe problème : les traits n'ont pas de taille
\n\nEn Rust, les traits sont des interfaces, mais contrairement à Java ou Go, ce ne sont pas des types de taille connue. Le compilateur a besoin de savoir combien de mémoire occupe la valeur retournée pour allouer de l'espace sur la pile. Or, la taille d'une implémentation spécifique d'itérateur (par exemple, std::iter::Map ou std::iter::Filter) peut varier.
Il est donc impossible de retourner directement un Iterator. Il faut choisir l'un des motifs suivants :
- \n
- Dispatch statique —
impl Iterator(retour d'un type concret) \n - Dispatch dynamique —
Box<dyn Iterator>(effacement de type) \n - Enum dispatch — votre propre enum pour tous les itérateurs possibles \n
- Génériques — une fonction qui accepte et retourne des types génériques \n
Chaque approche a son coût. Analysons-les avec des exemples concrets.
\n\nMéthode 1 : impl Iterator — l'étalon-or
\n\nLa façon la plus simple et la plus performante est d'utiliser impl Trait dans le type de retour. C'est du sucre syntaxique qui dit au compilateur : "je vais retourner un type concret implémentant le trait, mais déduis-le toi-même".
fn get_numbers() -> impl Iterator<Item = i32> {\n (0..10).map(|x| x * 2)\n}\n\nfn main() {\n for num in get_numbers() {\n println!("{}", num);\n }\n}\n\nAvantages :
\n- \n
- Performances maximales — dispatch statique, aucun appel virtuel \n
- Le compilateur peut inline le code et appliquer des optimisations \n
- Taille minimale du binaire résultant \n
Inconvénients :
\n- \n
- Le type retourné doit être unique — impossible de retourner
Mapdans un cas etFilterdans un autre \n - Le type peut être très complexe (ex :
Map<Filter<...>>), ce qui rend les messages d'erreur du compilateur difficiles à lire \n - Impossible de stocker dans une structure ou un vecteur — uniquement dans des fonctions \n
Cette méthode est idéale dans 90% des cas. Si votre fonction retourne toujours la même chaîne d'itérateurs — utilisez impl Iterator sans hésitation.
Méthode 2 : Box<dyn Iterator> — flexibilité au prix des performances
\n\nLorsque vous devez retourner différents types d'itérateurs selon les conditions (par exemple, lors de l'analyse des arguments de ligne de commande), le dispatch dynamique via Box<dyn Iterator> vient à la rescousse.
fn get_data(use_filter: bool) -> Box<dyn Iterator<Item = i32>> {\n let data = vec![1, 2, 3, 4, 5];\n if use_filter {\n