Les conversions en Rust : From, Into, TryFrom, FromStr et AsRef
Les programmes passent leur temps à convertir des choses. Le texte d’un formulaire devient un nombre. Un tuple de trois nombres devient une Color. Un type d’erreur devient un autre type d’erreur. Rust a une petite famille de traits standard pour ça, et une fois que vous les connaissez, vous les verrez partout dans du vrai code.
Voyez ces traits comme les adaptateurs que vous glissez dans votre valise pour un voyage à l’étranger. Chacun a un rôle précis. Certains s’adaptent toujours (From). Certains peuvent ne pas convenir, et vous le disent (TryFrom). Certains lisent une étiquette et fabriquent l’objet (FromStr). Et d’autres vous laissent simplement brancher ce que vous avez sous la main (AsRef). Cette page les présente un par un, sans se presser. Vous avez déjà rencontré as sur la page Conversion de types, et un premier aperçu de From sur la page Les traits courants : on va partir de là.
Pourquoi ne pas simplement utiliser as ?
Section intitulée « Pourquoi ne pas simplement utiliser as ? »as est rapide, mais il a deux limites. Il ne fonctionne qu’entre des types simples intégrés au langage, comme les nombres, char et bool. Et il n’échoue jamais : si une valeur ne rentre pas, il vous donne discrètement un autre nombre.
fn main() { let items_in_cart: i32 = 300;
// `as` fait rentrer de force 300 dans un u8 et se trompe sans rien dire let squeezed = items_in_cart as u8; println!("avec as : {squeezed}");
// `try_from` remarque le problème et vous le signale let checked = u8::try_from(items_in_cart); println!("avec try_from : {:?}", checked);
// `from` n'existe que si la conversion ne peut jamais perdre de données let widened = i64::from(items_in_cart); println!("avec from : {widened}");}Sortie :
avec as : 44avec try_from : Err(TryFromIntError(PosOverflow))avec from : 300Ce 44, c’est le genre de bug qui reste caché pendant des mois. Les traits de conversion règlent le problème de trois façons :
- Ils sont honnêtes.
Fromn’est implémenté que lorsque rien ne peut mal tourner. Quand quelque chose peut mal tourner, on utiliseTryFrom, qui renvoie unResult. - Ils fonctionnent avec vos propres types. Vous ne pouvez pas écrire
(255, 0, 0) as Color, mais vous pouvez implémenterFrom<(u8, u8, u8)> for Color. - Le reste du code les comprend. L’opérateur
?,.parse()et beaucoup de fonctions de bibliothèques reposent sur ces traits.
Une bonne règle, donc : gardez as pour les petites conversions de nombres rapides et sans risque, et tournez-vous vers les traits partout ailleurs.
From et Into : les conversions qui marchent toujours
Section intitulée « From et Into : les conversions qui marchent toujours »From dit : « je sais me construire à partir de cet autre type ». Vous l’avez déjà utilisé. Beaucoup de types standard l’implémentent :
let greeting = String::from("bonjour"); // &str -> Stringlet big = i64::from(42_i32); // i32 -> i64, rentre toujourslet precise = f64::from(7_u8); // u8 -> f64, toujours exactlet scores = Vec::from([90, 85, 77]); // tableau -> Vec<i32>Remarquez ce qui manque : il n’existe pas de i32::from(some_i64). Un i64 peut ne pas rentrer dans un i32, donc Rust ne fait pas semblant que ça marche toujours. On y reviendra dans la partie sur TryFrom.
Implémenter From pour votre propre type
Section intitulée « Implémenter From pour votre propre type »Ici, une Color peut être construite à partir d’un tuple de valeurs rouge, vert et bleu :
#[derive(Debug)]struct Color { red: u8, green: u8, blue: u8,}
impl From<(u8, u8, u8)> for Color { fn from((red, green, blue): (u8, u8, u8)) -> Self { Color { red, green, blue } }}
fn main() { let orange = Color::from((255, 136, 0)); println!("{:?}", orange);
// On n'a jamais écrit `Into`, mais on l'obtient gratuitement : let sky: Color = (135, 206, 235).into(); println!("le ciel : r={} g={} b={}", sky.red, sky.green, sky.blue);}Sortie :
Color { red: 255, green: 136, blue: 0 }le ciel : r=135 g=206 b=235C’est là que ça devient agréable. La bibliothèque standard dit : « si B implémente From<A>, alors A implémente automatiquement Into<B> ». Un seul impl From vous donne donc deux façons de convertir :
Color::from(tuple)se lit « construis uneColorà partir de ça ».tuple.into()se lit « transforme ça en ce dont on a besoin ici ».
Pourquoi avoir écrit let sky: Color ? Parce que .into() ne dit pas en quoi convertir. En théorie, un tuple pourrait être converti en beaucoup de types. L’annotation indique à Rust lequel vous voulez. Si Rust voit déjà le type cible, par exemple grâce au paramètre d’une fonction, vous n’en avez pas besoin.
Vous verrez souvent .into() utilisé avec des paramètres de fonction. Une fonction comme fn new(title: impl Into<String>) accepte à la fois un &str et une String, et appelle title.into() à l’intérieur. Là, Rust connaît le type cible, donc aucune annotation n’est nécessaire. Vous essaierez ce motif vous-même dans l’exercice 2.
From dans la gestion des erreurs : comment ? convertit les erreurs
Section intitulée « From dans la gestion des erreurs : comment ? convertit les erreurs »Voici un endroit où From fait discrètement beaucoup de travail pour vous. Quand ? tombe sur un Err, il ne se contente pas de le renvoyer. Il appelle d’abord From::from sur l’erreur, pour la transformer dans le type d’erreur que renvoie votre fonction.
Si votre fonction renvoie votre propre AppError, et que AppError implémente From<ParseIntError>, alors ? peut transformer tout seul une erreur de lecture de nombre en AppError :
use std::num::ParseIntError;
#[derive(Debug)]enum AppError { MissingEquals, BadNumber(ParseIntError),}
impl From<ParseIntError> for AppError { fn from(e: ParseIntError) -> Self { AppError::BadNumber(e) }}
// Lit une ligne de configuration comme "max_users = 50"fn read_setting(line: &str) -> Result<(String, u32), AppError> { let (key, value) = line.split_once('=').ok_or(AppError::MissingEquals)?; let number = value.trim().parse::<u32>()?; // ParseIntError -> AppError Ok((key.trim().to_string(), number))}
fn main() { println!("{:?}", read_setting("max_users = 50")); println!("{:?}", read_setting("max_users = beaucoup")); println!("{:?}", read_setting("max_users 50"));}Sortie :
Ok(("max_users", 50))Err(BadNumber(ParseIntError { kind: InvalidDigit }))Err(MissingEquals)Sans l’impl From, ce deuxième ? ne compilerait pas, car Rust ne saurait pas comment transformer une ParseIntError en AppError. Avec, le corps de votre fonction reste propre. La page Les erreurs personnalisées explique plus en détail comment concevoir des types d’erreur, et la page anyhow et thiserror présente des crates qui écrivent ces impl From à votre place.
TryFrom et TryInto : les conversions qui peuvent échouer
Section intitulée « TryFrom et TryInto : les conversions qui peuvent échouer »Certaines conversions ne marchent que parfois. 300 ne rentre pas dans un u8. -5 n’est pas un pourcentage valide. Pour ces cas-là, Rust propose TryFrom. Il fonctionne comme From, mais renvoie un Result : vous êtes donc obligé de traiter le cas du « non ». Et c’est une bonne chose, car l’échec ne peut pas passer inaperçu.
Vous avez déjà vu la version intégrée en haut de cette page : u8::try_from(300_i32) renvoie un Err au lieu d’un mauvais nombre. Et de même que From est accompagné de Into, TryFrom est accompagné de TryInto, donc let small: Result<u8, _> = big.try_into(); fonctionne aussi. TryInto fait partie du prélude depuis l’édition 2021 : vous n’avez rien à importer.
Implémenter TryFrom pour votre propre type
Section intitulée « Implémenter TryFrom pour votre propre type »Créons un type Percentage qui ne peut contenir que des valeurs de 0 à 100. Si chaque Percentage est créé avec try_from, alors chaque Percentage de votre programme est valide. Vous vérifiez une fois, à l’entrée, et plus jamais ensuite.
#[derive(Debug)]struct Percentage(u8);
#[derive(Debug)]struct OutOfRange(i32);
impl TryFrom<i32> for Percentage { type Error = OutOfRange;
fn try_from(value: i32) -> Result<Self, Self::Error> { if (0..=100).contains(&value) { Ok(Percentage(value as u8)) // sans risque : on vient de vérifier l'intervalle } else { Err(OutOfRange(value)) } }}
fn main() { println!("{:?}", Percentage::try_from(85)); println!("{:?}", Percentage::try_from(150));
let battery: Result<Percentage, _> = (-5).try_into(); println!("{:?}", battery);}Sortie :
Ok(Percentage(85))Err(OutOfRange(150))Err(OutOfRange(-5))La ligne type Error = OutOfRange; indique à Rust quel type d’erreur renvoie cette conversion. C’est vous qui le choisissez. Ici, il transporte la mauvaise valeur, pour que celui qui reçoit l’erreur puisse afficher un message utile.
FromStr et .parse() : construire des valeurs à partir de texte
Section intitulée « FromStr et .parse() : construire des valeurs à partir de texte »Le texte est ce que vous convertirez le plus souvent. Les fichiers, les arguments de la ligne de commande et les formulaires web vous donnent tous des chaînes de caractères. Le trait FromStr décrit « comment construire ce type à partir d’un &str », et .parse() est la façon pratique de l’appeler.
Quand vous écrivez "42".parse::<i32>(), Rust va chercher l’implémentation de FromStr pour i32 et l’exécute. La partie ::<i32> (qu’on surnomme le « turbofish ») indique à Rust quel type vous voulez. Une annotation de type sur la variable fonctionne aussi.
let age = "42".parse::<i32>(); // Ok(42)let price: Result<f64, _> = "4.99".parse(); // Ok(4.99)let broken = "quarante-deux".parse::<i32>(); // Err(ParseIntError { kind: InvalidDigit })Chaque appel renvoie un Result, parce qu’un texte peut toujours être incorrect.
Implémenter FromStr pour votre propre type
Section intitulée « Implémenter FromStr pour votre propre type »Imaginez un encadreur dont les clients saisissent des dimensions comme "30x40". Apprenons à Rust à lire ce format :
use std::str::FromStr;
#[derive(Debug)]struct Size { width: u32, height: u32,}
#[derive(Debug)]enum SizeError { MissingX, BadNumber,}
impl FromStr for Size { type Err = SizeError;
fn from_str(text: &str) -> Result<Self, Self::Err> { let (w, h) = text.split_once('x').ok_or(SizeError::MissingX)?; let width = w.trim().parse().map_err(|_| SizeError::BadNumber)?; let height = h.trim().parse().map_err(|_| SizeError::BadNumber)?; Ok(Size { width, height }) }}
fn main() { println!("{:?}", "30x40".parse::<Size>());
println!("{:?}", "30 sur 40".parse::<Size>()); println!("{:?}", "30xgrand".parse::<Size>());}Sortie :
Ok(Size { width: 30, height: 40 })Err(MissingX)Err(BadNumber)Deux petits détails. D’abord, FromStr appelle son type d’erreur Err, et non Error comme TryFrom. C’est une vieille bizarrerie de nommage : ne vous inquiétez pas si vous les confondez au début, le compilateur vous le rappellera. Ensuite, il faut use std::str::FromStr; pour implémenter le trait, mais pas pour appeler .parse().
.map_err(|_| SizeError::BadNumber) remplace l’erreur de lecture du nombre par notre propre erreur. C’est une alternative pratique à un impl From quand vous n’en avez besoin qu’à un seul endroit.
AsRef : accepter tout ce qui peut vous prêter une vue
Section intitulée « AsRef : accepter tout ce qui peut vous prêter une vue »Les traits vus jusqu’ici créent une nouvelle valeur. AsRef est différent. Il ne construit rien. Il dit simplement : « je peux vous prêter, à peu de frais, une vue de moi-même sous la forme de ce type ».
À quoi ça sert ? Imaginez une fonction qui compte les mots. Si elle prend un &str, un appelant qui a une String doit écrire &name. Si elle prend une String, un appelant qui a un &str doit allouer une nouvelle String. Avec impl AsRef<str>, tout fonctionne directement :
fn word_count(text: impl AsRef<str>) -> usize { text.as_ref().split_whitespace().count()}
fn main() { let note = String::from("acheter du pain frais");
println!("{}", word_count("bonjour le monde")); // &str println!("{}", word_count(¬e)); // &String println!("{}", word_count(note)); // String}Sortie :
344À l’intérieur de la fonction, text.as_ref() vous donne un simple &str, quel que soit ce que l’appelant a passé.
AsRef dans la bibliothèque standard : les chemins
Section intitulée « AsRef dans la bibliothèque standard : les chemins »Vous avez sans doute déjà utilisé AsRef sans le remarquer. Les fonctions de fichiers de std::fs ressemblent à ceci :
pub fn read_to_string<P: AsRef<Path>>(path: P) -> io::Result<String>C’est pour ça que fs::read_to_string("notes.txt"), fs::read_to_string(my_string) et fs::read_to_string(my_path_buf) fonctionnent tous. Vous pouvez utiliser la même astuce dans votre propre code. Cet exemple ne regarde que les noms de chemins, il ne touche donc à aucun vrai fichier :
use std::path::{Path, PathBuf};
fn extension_of(path: impl AsRef<Path>) -> String { match path.as_ref().extension() { Some(ext) => ext.to_string_lossy().to_string(), None => String::from("(aucune)"), }}
fn main() { println!("{}", extension_of("notes.txt")); println!("{}", extension_of(String::from("vacances.jpeg"))); println!("{}", extension_of(PathBuf::from("sauvegardes/2024/site.tar.gz"))); println!("{}", extension_of("README"));}Sortie :
txtjpeggz(aucune)AsMut repose sur la même idée, mais prête une vue mutable. Par exemple, un Vec et un tableau peuvent tous les deux se prêter sous forme de slice (tranche) mutable :
fn reset_all(scores: &mut impl AsMut<[u32]>) { for score in scores.as_mut() { *score = 0; }}
// Ces deux appels fonctionnent :// reset_all(&mut vec![12, 7, 30]);// reset_all(&mut [3, 1, 4]);Vous utiliserez AsMut beaucoup moins souvent que AsRef. Il suffit de savoir le reconnaître quand vous le croisez.
Lequel utiliser ?
Section intitulée « Lequel utiliser ? »Si vous hésitez, ce tableau est un bon point de départ :
| Vous voulez… | Utilisez | Exemple |
|---|---|---|
| Convertir rapidement entre types numériques, en acceptant la troncature | as |
x as f64 |
| Convertir quand ça ne peut jamais échouer | From |
i64::from(n), String::from("salut") |
| Pareil, quand le type cible est déjà connu | Into |
let s: String = "salut".into(); |
| Convertir quand ça peut échouer | TryFrom / TryInto |
u8::try_from(300) |
| Construire une valeur à partir de texte | FromStr / .parse() |
"42".parse::<i32>() |
| Accepter à peu de frais de nombreuses entrées de type chaîne ou chemin | AsRef |
fn f(p: impl AsRef<Path>) |
| Transformer une valeur en texte lisible par des humains | Display → .to_string() |
42.to_string() |
La dernière ligne vient gratuitement : implémentez Display (voir Les traits courants) et chaque valeur de votre type obtient une méthode .to_string().
Erreurs courantes
Section intitulée « Erreurs courantes »Appeler .into() quand Rust ne peut pas deviner le type cible.
fn main() { let name = "Ferris".into(); println!("{name}");}error[E0283]: type annotations needed --> src/main.rs:2:9 |2 | let name = "Ferris".into(); | ^^^^ ---- type must be known at this point | = note: the type must implement `From<&str>` = note: required for `&str` to implement `Into<_>`help: consider giving `name` an explicit type |2 | let name: /* Type */ = "Ferris".into(); | ++++++++++++Le compilateur réclame une annotation de type : il doit connaître le type à ce moment-là. Un &str peut devenir une String, une Box<str>, un Vec<u8> et bien d’autres choses. Rust ne devinera pas. Ajoutez le type : let name: String = "Ferris".into();, ou écrivez simplement String::from("Ferris").
Implémenter Into au lieu de From. On dirait que c’est la même chose, donc le piège est facile :
use std::num::ParseIntError;
#[derive(Debug)]struct AppError(String);
impl Into<AppError> for ParseIntError { fn into(self) -> AppError { AppError(self.to_string()) }}
fn read_port(text: &str) -> Result<u16, AppError> { let port = text.parse::<u16>()?; Ok(port)}error[E0277]: `?` couldn't convert the error to `AppError` --> src/main.rs:13:35 |12 | fn read_port(text: &str) -> Result<u16, AppError> { | --------------------- expected `AppError` because of this13 | let port = text.parse::<u16>()?; | --------------^ the trait `From<ParseIntError>` is not implemented for `AppError` | = note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` traitLe message dit que ? n’a pas pu convertir l’erreur en AppError, car From<ParseIntError> n’est pas implémenté pour AppError. Le cadeau ne marche que dans un sens : implémenter From vous donne Into, mais implémenter Into ne vous donne pas From. Or ?, comme beaucoup de code de bibliothèque, demande From. Implémentez donc toujours From, et laissez Into venir tout seul. (Clippy, l’outil d’analyse de code de Rust, vous avertit même dans ce cas.)
Exercices
Section intitulée « Exercices »1. Lire une heure
Section intitulée « 1. Lire une heure »Créez une struct Time avec hour: u8 et minute: u8. Implémentez FromStr pour que "09:45".parse::<Time>() fonctionne. Renvoyez une erreur s’il n’y a pas de :, si une partie n’est pas un nombre, ou si l’heure dépasse 23 ou les minutes dépassent 59. Essayez avec "09:45", "25:00" et "midi". Indice : split_once(':') vous donne les deux moitiés. Une simple String suffit comme type d’erreur.
Voir la solution
use std::str::FromStr;
#[derive(Debug)]struct Time { hour: u8, minute: u8,}
impl FromStr for Time { type Err = String;
fn from_str(text: &str) -> Result<Self, Self::Err> { let (h, m) = text.split_once(':').ok_or("':' manquant")?; let hour: u8 = h.parse().map_err(|_| "l'heure n'est pas un nombre")?; let minute: u8 = m.parse().map_err(|_| "les minutes ne sont pas un nombre")?;
if hour > 23 || minute > 59 { return Err(format!("{text} n'est pas une heure valide")); } Ok(Time { hour, minute }) }}
fn main() { for input in ["09:45", "25:00", "midi"] { match input.parse::<Time>() { Ok(t) => println!("{input} -> {} h {} min", t.hour, t.minute), Err(e) => println!("{input} -> erreur : {e}"), } }}Sortie :
09:45 -> 9 h 45 min25:00 -> erreur : 25:00 n'est pas une heure validemidi -> erreur : ':' manquantLe ? après .ok_or("':' manquant") fonctionne parce que String implémente From<&str>. C’est la même astuce From que dans la partie sur la gestion des erreurs, avec un type standard cette fois.
2. Un minuteur de cuisine
Section intitulée « 2. Un minuteur de cuisine »Créez une struct Timer avec minutes: u32 et seconds: u32. Implémentez From<u32> pour qu’un nombre total de secondes devienne un Timer (150 devient 2 minutes 30 secondes). Écrivez ensuite fn start(timer: impl Into<Timer>) qui affiche la durée, et appelez-la avec 150 et avec Timer { minutes: 1, seconds: 5 }. Indice : les minutes valent total / 60 et le reste vaut total % 60. Tout type se convertit déjà en lui-même, donc passer un Timer fonctionne aussi.
Voir la solution
struct Timer { minutes: u32, seconds: u32,}
impl From<u32> for Timer { fn from(total: u32) -> Self { Timer { minutes: total / 60, seconds: total % 60 } }}
fn start(timer: impl Into<Timer>) { let t = timer.into(); println!("Minuteur réglé sur {}:{:02}", t.minutes, t.seconds);}
fn main() { start(150_u32); start(Timer { minutes: 1, seconds: 5 });}Sortie :
Minuteur réglé sur 2:30Minuteur réglé sur 1:05On n’a écrit que From<u32>, et pourtant start accepte à la fois un nombre et un Timer. Le nombre passe par notre From, et le Timer fonctionne parce que la bibliothèque standard prévoit que tout type se convertit en lui-même.
Récapitulatif
Section intitulée « Récapitulatif »assert aux conversions rapides entre nombres et peut perdre des données sans rien dire. Les traits de conversion sont explicites, sûrs, et fonctionnent avec vos propres types.- Implémentez
Frompour les conversions qui marchent toujours, et vous obtenezIntogratuitement..into()a besoin de connaître son type cible. - L’opérateur
?appelleFrompour convertir les types d’erreur :impl From<OtherError> for MyErrorgarde donc la gestion des erreurs concise. - Utilisez
TryFrom/TryIntoquand une conversion peut échouer. Ils renvoient unResult. - Implémentez
FromStrpour que.parse::<VotreType>()fonctionne sur du texte. - Utilisez
impl AsRef<str>ouimpl AsRef<Path>pour accepter à peu de frais de nombreux types d’entrée, sans copie.
Petit quiz
Section intitulée « Petit quiz »Petit quiz
1. Vous implémentez From<Celsius> for Fahrenheit. Qu'obtenez-vous d'autre automatiquement ?
2. Quel appel est la façon sûre de transformer en u8 un i32 qui vaut peut-être 300 ?
3. Que devez-vous implémenter pour que "30x40".parse::<Size>() fonctionne ?
4. Pourquoi let name = "Ferris".into(); ne compile-t-il pas ?
Score : 0 / 4
Pour aller plus loin
Section intitulée « Pour aller plus loin »std::convert— documentation de la bibliothèque standard (en anglais) : le module deFrom,Into,TryFrom,TryInto,AsRefetAsMut, avec un court guide pour savoir quand implémenter chacun.FromStr— documentation de la bibliothèque standard (en anglais) : le trait derrière.parse(), avec un exemple complet d’implémentation.AsRef— documentation de la bibliothèque standard (en anglais) : en quoiAsRefdiffère deBorrow, et pourquoi on l’utilise pour les conversions de références peu coûteuses.- Conversion — Rust by Example (en anglais) : de courts chapitres exécutables sur
From/Into,TryFrom/TryInto, et la conversion vers et depuis des chaînes. - Conversions — Rust API Guidelines (en anglais) : les recommandations de la communauté sur les traits de conversion que vos propres types devraient implémenter.