Introduction : Le piège dans lequel tombent tous les débutants en Rust
\n\nVous apprenez Rust. Vous avez déjà compris le concept de propriété et d'emprunt. Vous écrivez votre première fonction sérieuse et vous pensez : « Ah, pour ne pas déplacer la chaîne, je vais passer une référence ! ». Et vous écrivez :
\n\nfn print_string(s: &String) {\n println!("{}", s);\n}\n\nLe compilateur est content. Le code fonctionne. Vous êtes content. Mais voilà le problème : un développeur Rust expérimenté, en voyant votre code, fera la grimace. Pourquoi ? Puisque tout fonctionne !
\n\nCette question est l'une des plus populaires sur StackOverflow. Des milliers de développeurs trébuchent sur ce détail apparemment insignifiant. Aujourd'hui, nous allons comprendre pourquoi &String, &Vec et &Box dans la signature d'une fonction sont un signal d'alarme, et comment écrire correctement.
Problème n°1 : Limitation implicite des types (Type Coercion)
\n\nRust possède un puissant système de conversion automatique de types, mais uniquement dans un sens. Cela s'appelle Deref Coercion (conversion par déréférencement).
\n\nLe principe est simple : si vous avez une référence vers String, Rust peut automatiquement la transformer en référence vers &str. Idem avec &Vec<T> → &[T] et &Box<T> → &T.
Regardez la différence :
\n\n// Mauvais : n'accepte que &String\nfn bad(s: &String) {\n println!("{}", s);\n}\n\n// Bon : accepte &str\nfn good(s: &str) {\n println!("{}", s);\n}\n\nfn main() {\n let owned = String::from("Bonjour le monde !");\n let slice = "Bonjour, slice !";\n \n bad(&owned); // Fonctionne\n // bad(slice); // ERREUR ! slice est un &str, pas un &String\n \n good(&owned); // Fonctionne (conversion automatique)\n good(slice); // Fonctionne (c'est déjà un &str)\n}\n\nVous voyez ? La fonction good accepte n'importe quelle chaîne : aussi bien le propriétaire String, que le littéral de chaîne &str, et même une partie de chaîne. La fonction bad n'accepte que String. C'est une limitation inutile.
Exemple pratique : recherche de sous-chaîne
\n\n// Recherche limitée\nfn find_in_string(haystack: &String, needle: &str) -> bool {\n haystack.contains(needle)\n}\n\n// Recherche universelle\nfn find_in_str(haystack: &str, needle: &str) -> bool {\n haystack.contains(needle)\n}\n\nfn main() {\n let my_string = String::from("Rust est génial");\n let my_str = "Rust est génial";\n \n // Fonctionne uniquement avec String\n find_in_string(&my_string, "Rust");\n // find_in_str(my_str, "Rust"); // Erreur de compilation !\n \n // Fonctionne avec les deux\n find_in_str(&my_string, "Rust");\n find_in_str(my_str, "Rust");\n}\n\nProblème n°2 : Performance et indirections superflues
\n\nRegardons « sous le capot ». Qu'est-ce qu'un String en mémoire ? C'est une structure composée de trois champs :
- \n
- Un pointeur vers les données sur le tas \n
- La longueur de la chaîne \n
- La capacité (capacity) \n
Lorsque vous passez &String, vous passez une référence vers cette structure. Pour accéder aux données, le compilateur doit :
- \n
- Déréférencer
&String→ obtenir la structureString\n - Depuis la structure, prendre le pointeur vers les données sur le tas \n
- Déréférencer ce pointeur → obtenir les octets de la chaîne \n
Lorsque vous passez &str, vous passez un fat pointer (pointeur large), qui se compose de seulement deux champs :
- \n
- Un pointeur vers les données \n
- La longueur \n
Le compilateur n'a besoin de faire qu'une seule étape — déréférencer le pointeur. Moins d'indirection — code plus rapide. La différence est microscopique, mais dans des boucles chaudes ou lors du passage à