Conversioni in Rust: From, Into, TryFrom, FromStr e AsRef
I programmi convertono cose in continuazione. Il testo di un modulo diventa un numero. Una tupla di tre numeri diventa un Color. Un tipo di errore ne diventa un altro. Rust ha una piccola famiglia di trait standard per questo, e quando li conosci li vedi ovunque nel codice vero.
Pensa a questi trait come agli adattatori che metti in valigia per un viaggio all’estero. Ognuno è fatto per un compito. Alcuni vanno sempre bene (From). Altri potrebbero non andare bene, e te lo dicono (TryFrom). Altri leggono un’etichetta e costruiscono l’oggetto (FromStr). E altri ancora ti lasciano collegare qualsiasi cosa tu abbia (AsRef). Questa pagina li presenta uno per uno, con calma. Hai già incontrato as nella pagina sul cast dei tipi e un primo assaggio di From nella pagina sui trait comuni, quindi partiamo da lì.
Perché non usare semplicemente as?
Sezione intitolata “Perché non usare semplicemente as?”as è veloce, ma ha due limiti. Funziona solo tra tipi semplici predefiniti, come numeri, char e bool. E non fallisce mai: se un valore non ci sta, ti dà in silenzio un numero diverso.
fn main() { let items_in_cart: i32 = 300;
// `as` schiaccia 300 in un u8 e sbaglia senza dire niente let squeezed = items_in_cart as u8; println!("con as: {squeezed}");
// `try_from` si accorge del problema e te lo dice let checked = u8::try_from(items_in_cart); println!("con try_from: {:?}", checked);
// `from` esiste solo quando la conversione non può mai perdere dati let widened = i64::from(items_in_cart); println!("con from: {widened}");}Output:
con as: 44con try_from: Err(TryFromIntError(PosOverflow))con from: 300Quel 44 è il tipo di bug che resta nascosto per mesi. (PosOverflow vuol dire che il valore è troppo grande, in positivo, per il tipo di destinazione.) I trait di conversione risolvono il problema in tre modi:
- Sono onesti.
Fromviene implementato solo quando non può andare storto niente. Quando qualcosa può andare storto, usiTryFrom, che restituisce unResult. - Funzionano con i tuoi tipi. Non puoi scrivere
(255, 0, 0) as Color, ma puoi implementareFrom<(u8, u8, u8)> for Color. - Il resto del codice li capisce. L’operatore
?,.parse()e tante funzioni di libreria sono costruiti su questi trait.
Quindi una buona regola pratica: tieni as per i trucchi veloci e sicuri con i numeri, e usa i trait in tutti gli altri casi.
From e Into: conversioni che funzionano sempre
Sezione intitolata “From e Into: conversioni che funzionano sempre”From dice «so costruirmi a partire da quell’altro tipo». L’hai già usato. Molti tipi standard lo implementano:
let greeting = String::from("ciao"); // &str -> Stringlet big = i64::from(42_i32); // i32 -> i64, ci sta semprelet precise = f64::from(7_u8); // u8 -> f64, sempre esattolet scores = Vec::from([90, 85, 77]); // array -> Vec<i32>Nota cosa manca: non esiste i32::from(some_i64). Un i64 potrebbe non stare in un i32, quindi Rust non fa finta che funzioni sempre. Ci torniamo nella sezione su TryFrom.
Implementare From per un tuo tipo
Sezione intitolata “Implementare From per un tuo tipo”Qui un Color si può costruire da una tupla con i valori di rosso, verde e blu:
#[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);
// Non abbiamo mai scritto `Into`, ma lo otteniamo gratis: let sky: Color = (135, 206, 235).into(); println!("il cielo è r={} g={} b={}", sky.red, sky.green, sky.blue);}Output:
Color { red: 255, green: 136, blue: 0 }il cielo è r=135 g=206 b=235Ecco la parte bella. La libreria standard dice: «se B implementa From<A>, allora A implementa automaticamente Into<B>». Quindi un solo impl From ti dà due modi di convertire:
Color::from(tuple)si legge «costruisci unColorda questo».tuple.into()si legge «trasforma questo in quello che serve qui».
Perché abbiamo scritto let sky: Color? Perché .into() non dice in cosa trasformare. In teoria una tupla si potrebbe convertire in tanti tipi. L’annotazione dice a Rust quale intendi. Se Rust vede già il tipo di destinazione, per esempio dal parametro di una funzione, non ti serve.
Vedrai spesso .into() anche nei parametri delle funzioni. Una funzione come fn new(title: impl Into<String>) accetta sia un &str sia una String, e dentro chiama title.into(). Lì Rust conosce la destinazione, quindi non serve nessuna annotazione. Proverai tu stesso questo schema nell’esercizio 2 della Pratica.
From nella gestione degli errori: come ? converte gli errori
Sezione intitolata “From nella gestione degli errori: come ? converte gli errori”Ecco un punto in cui From lavora tanto per te senza farsi notare. Quando ? incontra un Err, non si limita a restituirlo. Prima chiama From::from sull’errore, per trasformarlo nel tipo di errore che restituisce la tua funzione.
Quindi, se la tua funzione restituisce un tuo AppError e AppError implementa From<ParseIntError>, allora ? può trasformare da solo un errore di conversione in un 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) }}
// Legge una riga di configurazione come "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 = tanti")); println!("{:?}", read_setting("max_users 50"));}Output:
Ok(("max_users", 50))Err(BadNumber(ParseIntError { kind: InvalidDigit }))Err(MissingEquals)Senza l’impl From, il secondo ? non compilerebbe, perché Rust non saprebbe come trasformare un ParseIntError in un AppError. Con l’impl, il corpo della funzione resta pulito. La pagina sugli errori personalizzati approfondisce come progettare i tipi di errore, e quella su anyhow e thiserror mostra i crate che scrivono questi impl From al posto tuo.
TryFrom e TryInto: conversioni che possono fallire
Sezione intitolata “TryFrom e TryInto: conversioni che possono fallire”Alcune conversioni funzionano solo a volte. 300 non sta in un u8. -5 non è una percentuale valida. Per questi casi Rust ha TryFrom. Funziona come From, ma restituisce un Result, quindi devi gestire il caso «no». Ed è un bene: il fallimento non può sfuggirti.
Hai già visto la versione predefinita all’inizio di questa pagina: u8::try_from(300_i32) restituisce un Err invece di un numero sbagliato. E così come From porta con sé Into, TryFrom porta con sé TryInto, quindi funziona anche let small: Result<u8, _> = big.try_into();. TryInto è nel prelude dall’edizione 2021, quindi non devi importare niente.
Implementare TryFrom per un tuo tipo
Sezione intitolata “Implementare TryFrom per un tuo tipo”Creiamo un tipo Percentage che può contenere solo valori da 0 a 100. Se ogni Percentage viene creata tramite try_from, allora ogni Percentage nel tuo programma è valida. Controlli una volta, all’ingresso, e mai più.
#[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)) // sicuro: abbiamo appena controllato l'intervallo } 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);}Output:
Ok(Percentage(85))Err(OutOfRange(150))Err(OutOfRange(-5))La riga type Error = OutOfRange; dice a Rust che tipo di errore restituisce questa conversione. Lo scegli tu. Qui contiene il valore sbagliato, così chi riceve l’errore può mostrare un messaggio utile.
FromStr e .parse(): costruire valori dal testo
Sezione intitolata “FromStr e .parse(): costruire valori dal testo”Il testo è la cosa che convertirai più spesso. File, argomenti della riga di comando e moduli web ti danno tutti delle stringhe. Il trait FromStr descrive «come costruire questo tipo da un &str», e .parse() è il modo comodo per chiamarlo.
Quando scrivi "42".parse::<i32>(), Rust cerca l’implementazione di FromStr per i32 e la esegue. La parte ::<i32> (la chiamano «turbofish») dice a Rust quale tipo vuoi. Funziona anche un’annotazione di tipo sulla variabile.
let age = "42".parse::<i32>(); // Ok(42)let price: Result<f64, _> = "4.99".parse(); // Ok(4.99)let broken = "quarantadue".parse::<i32>(); // Err(ParseIntError { kind: InvalidDigit })Ogni chiamata restituisce un Result, perché il testo può sempre essere sbagliato.
Implementare FromStr per un tuo tipo
Sezione intitolata “Implementare FromStr per un tuo tipo”Immagina un negozio di cornici dove i clienti scrivono misure come "30x40". Insegniamo a Rust a leggerle:
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 per 40".parse::<Size>()); println!("{:?}", "30xgrande".parse::<Size>());}Output:
Ok(Size { width: 30, height: 40 })Err(MissingX)Err(BadNumber)Due piccoli dettagli. Primo: FromStr chiama il suo tipo di errore Err, non Error come fa TryFrom. È una vecchia stranezza nei nomi, quindi non preoccuparti se all’inizio li confondi: il compilatore te lo ricorderà. Secondo: ti serve use std::str::FromStr; per implementare il trait, ma non per chiamare .parse().
.map_err(|_| SizeError::BadNumber) sostituisce l’errore di conversione del numero con il nostro errore. È un’alternativa comoda a scrivere un impl From quando ti serve in un solo punto.
AsRef: accettare tutto ciò che può prestarti una vista
Sezione intitolata “AsRef: accettare tutto ciò che può prestarti una vista”I trait visti finora creano un valore nuovo. AsRef è diverso. Non costruisce niente. Dice solo «posso darti una vista economica e presa in prestito di me stesso come quel tipo».
Perché dovresti volerlo? Mettiamo che tu scriva una funzione che conta le parole. Se prende &str, chi ha una String deve scrivere &name. Se prende String, chi ha un &str deve allocare una nuova String. Con impl AsRef<str>, funzionano tutti:
fn word_count(text: impl AsRef<str>) -> usize { text.as_ref().split_whitespace().count()}
fn main() { let note = String::from("compra latte e uova");
println!("{}", word_count("ciao mondo rust")); // &str println!("{}", word_count(¬e)); // &String println!("{}", word_count(note)); // String}Output:
344Dentro la funzione, text.as_ref() ti dà un semplice &str, qualunque cosa abbia passato chi chiama.
AsRef nella libreria standard: i percorsi
Sezione intitolata “AsRef nella libreria standard: i percorsi”Probabilmente hai già usato AsRef senza accorgertene. Le funzioni sui file in std::fs sono fatte così:
pub fn read_to_string<P: AsRef<Path>>(path: P) -> io::Result<String>Ecco perché fs::read_to_string("notes.txt"), fs::read_to_string(my_string) e fs::read_to_string(my_path_buf) funzionano tutti. Puoi usare lo stesso trucco nel tuo codice. Questo esempio guarda solo i nomi dei percorsi, quindi non tocca nessun file vero:
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("(nessuna)"), }}
fn main() { println!("{}", extension_of("appunti.txt")); println!("{}", extension_of(String::from("vacanze.jpeg"))); println!("{}", extension_of(PathBuf::from("backup/2024/sito.tar.gz"))); println!("{}", extension_of("README"));}Output:
txtjpeggz(nessuna)AsMut segue la stessa idea, ma presta una vista mutabile. Per esempio, sia un Vec sia un array possono prestarsi come slice mutabile:
fn reset_all(scores: &mut impl AsMut<[u32]>) { for score in scores.as_mut() { *score = 0; }}
// Funzionano entrambe:// reset_all(&mut vec![12, 7, 30]);// reset_all(&mut [3, 1, 4]);Userai AsMut molto meno spesso di AsRef. Basta che tu lo riconosca quando lo vedi.
Quale uso?
Sezione intitolata “Quale uso?”Quando non sei sicuro, questa tabella è un buon punto di partenza:
| Vuoi… | Usa | Esempio |
|---|---|---|
| Convertire al volo tra tipi numerici, accettando il troncamento | as |
x as f64 |
| Convertire quando non può mai fallire | From |
i64::from(n), String::from("ciao") |
| Lo stesso, quando il tipo di destinazione è già noto | Into |
let s: String = "ciao".into(); |
| Convertire quando potrebbe fallire | TryFrom / TryInto |
u8::try_from(300) |
| Costruire un valore dal testo | FromStr / .parse() |
"42".parse::<i32>() |
| Accettare in modo economico tanti input simili a stringhe o percorsi | AsRef |
fn f(p: impl AsRef<Path>) |
| Trasformare un valore in testo leggibile per le persone | Display → .to_string() |
42.to_string() |
L’ultima riga arriva gratis: implementa Display (vedi Trait comuni) e ogni valore del tuo tipo ottiene un metodo .to_string().
Errori comuni
Sezione intitolata “Errori comuni”Chiamare .into() quando Rust non può capire il tipo di destinazione.
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(); | ++++++++++++Il compilatore dice (in inglese) che servono annotazioni di tipo e ti suggerisce di dare a name un tipo esplicito. Un &str può diventare una String, un Box<str>, un Vec<u8> e altro ancora. Rust non tira a indovinare. Aggiungi il tipo: let name: String = "Ferris".into();, oppure scrivi semplicemente String::from("Ferris").
Implementare Into invece di From. Sembra che dovrebbe essere la stessa cosa, quindi è una trappola 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` traitIl messaggio spiega che ? non è riuscito a convertire l’errore in AppError, perché manca From<ParseIntError>, e ricorda che ? converte sempre gli errori tramite il trait From. Il regalo vale in una sola direzione: implementare From ti dà Into, ma implementare Into non ti dà From. E ?, insieme a tanto codice di libreria, chiede From. Quindi implementa sempre From, e lascia che Into arrivi da solo. (Anche Clippy, il linter di Rust, ti avvisa in questo caso.)
Pratica
Sezione intitolata “Pratica”1. Leggere un orario
Sezione intitolata “1. Leggere un orario”Crea una struct Time con hour: u8 e minute: u8. Implementa FromStr in modo che "09:45".parse::<Time>() funzioni. Restituisci un errore se manca il :, se una parte non è un numero, o se l’ora supera 23 o i minuti superano 59. Provala con "09:45", "25:00" e "pranzo". Suggerimento: split_once(':') ti dà le due metà. Come tipo di errore va bene una semplice String.
Mostra la soluzione
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("manca ':'")?; let hour: u8 = h.parse().map_err(|_| "l'ora non è un numero")?; let minute: u8 = m.parse().map_err(|_| "i minuti non sono un numero")?;
if hour > 23 || minute > 59 { return Err(format!("{text} non è un orario vero")); } Ok(Time { hour, minute }) }}
fn main() { for input in ["09:45", "25:00", "pranzo"] { match input.parse::<Time>() { Ok(t) => println!("{input} -> {}h {}m", t.hour, t.minute), Err(e) => println!("{input} -> errore: {e}"), } }}Output:
09:45 -> 9h 45m25:00 -> errore: 25:00 non è un orario veropranzo -> errore: manca ':'Il ? dopo .ok_or("manca ':'") funziona perché String implementa From<&str>. È lo stesso trucco di From della sezione sulla gestione degli errori, questa volta con un tipo standard.
2. Un timer da cucina
Sezione intitolata “2. Un timer da cucina”Crea una struct Timer con minutes: u32 e seconds: u32. Implementa From<u32> in modo che un numero totale di secondi diventi un Timer (150 diventa 2 minuti e 30 secondi). Poi scrivi fn start(timer: impl Into<Timer>) che stampa il tempo, e chiamala con 150 e con Timer { minutes: 1, seconds: 5 }. Suggerimento: i minuti sono total / 60 e il resto è total % 60. Ogni tipo si converte già in se stesso, quindi funziona anche passare un Timer.
Mostra la soluzione
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!("Timer impostato su {}:{:02}", t.minutes, t.seconds);}
fn main() { start(150_u32); start(Timer { minutes: 1, seconds: 5 });}Output:
Timer impostato su 2:30Timer impostato su 1:05Abbiamo scritto solo From<u32>, eppure start accetta sia un numero sia un Timer. Il numero funziona grazie al nostro From, e il Timer funziona perché la libreria standard dice che ogni tipo si converte in se stesso.
Riepilogo
Sezione intitolata “Riepilogo”asserve per cast numerici veloci e può perdere dati in silenzio. I trait di conversione sono espliciti, sicuri e funzionano con i tuoi tipi.- Implementa
Fromper le conversioni che funzionano sempre, e ottieniIntogratis..into()deve conoscere il tipo di destinazione. - L’operatore
?chiamaFromper convertire i tipi di errore, quindiimpl From<AltroErrore> for MioErroremantiene corta la gestione degli errori. - Usa
TryFrom/TryIntoquando una conversione può fallire. Restituiscono unResult. - Implementa
FromStrper far funzionare.parse::<TuoTipo>()sul testo. - Usa
impl AsRef<str>oimpl AsRef<Path>per accettare tanti tipi di input in modo economico, senza copiare.
Verifica veloce
Sezione intitolata “Verifica veloce”Verifica veloce
1. Implementi From<Celsius> for Fahrenheit. Cos'altro ottieni automaticamente?
2. Qual è il modo sicuro per trasformare in un u8 un i32 che potrebbe valere 300?
3. Cosa implementi perché "30x40".parse::<Size>() funzioni?
4. Perché let name = "Ferris".into(); non compila?
Punteggio: 0 / 4
Per approfondire
Sezione intitolata “Per approfondire”std::convert— documentazione della libreria standard (in inglese): la casa diFrom,Into,TryFrom,TryInto,AsRefeAsMut, con una breve guida su quando implementare ciascuno.FromStr— documentazione della libreria standard (in inglese): il trait dietro.parse(), con un esempio completo di implementazione.AsRef— documentazione della libreria standard (in inglese): in cosaAsRefdifferisce daBorrowe perché si usa per conversioni economiche tra riferimenti.- Conversion — Rust by Example (in inglese): brevi capitoli eseguibili su
From/Into,TryFrom/TryIntoe sulle conversioni da e verso stringhe. - Conversions — Rust API Guidelines (in inglese): i consigli della comunità su quali trait di conversione dovrebbero implementare i tuoi tipi.