Aller au contenu

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à.

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 : 44
avec try_from : Err(TryFromIntError(PosOverflow))
avec from : 300
▶ Essayez par vous-même

Ce 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. From n’est implémenté que lorsque rien ne peut mal tourner. Quand quelque chose peut mal tourner, on utilise TryFrom, qui renvoie un Result.
  • Ils fonctionnent avec vos propres types. Vous ne pouvez pas écrire (255, 0, 0) as Color, mais vous pouvez implémenter From<(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 -> String
let big = i64::from(42_i32); // i32 -> i64, rentre toujours
let precise = f64::from(7_u8); // u8 -> f64, toujours exact
let 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.

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=235
▶ Essayez par vous-même

C’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 une Color à 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)
▶ Essayez par vous-même

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.

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))
▶ Essayez par vous-même

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.

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)
▶ Essayez par vous-même

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(&note)); // &String
println!("{}", word_count(note)); // String
}

Sortie :

3
4
4
▶ Essayez par vous-même

À 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 :

txt
jpeg
gz
(aucune)
▶ Essayez par vous-même

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.

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().

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 this
13 | 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` trait

Le 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.)

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 min
25:00 -> erreur : 25:00 n'est pas une heure valide
midi -> erreur : ':' manquant
▶ Essayez par vous-même

Le ? 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.

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:30
Minuteur réglé sur 1:05
▶ Essayez par vous-même

On 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.

  • as sert 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 From pour les conversions qui marchent toujours, et vous obtenez Into gratuitement. .into() a besoin de connaître son type cible.
  • L’opérateur ? appelle From pour convertir les types d’erreur : impl From<OtherError> for MyError garde donc la gestion des erreurs concise.
  • Utilisez TryFrom / TryInto quand une conversion peut échouer. Ils renvoient un Result.
  • Implémentez FromStr pour que .parse::<VotreType>() fonctionne sur du texte.
  • Utilisez impl AsRef<str> ou impl AsRef<Path> pour accepter à peu de frais de nombreux types d’entrée, sans copie.

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