Trait comuni in Rust
Nella pagina sui trait hai scritto trait tuoi. Ma la libreria standard ha già un insieme di trait che quasi ogni programma Rust usa. Rispondono a domande semplici su un tipo: Posso stamparlo? Posso copiarlo? Posso confrontarlo? Posso crearne uno vuoto?
Pensali come distintivi su una divisa. Ogni distintivo dice «questo tipo sa fare X». Molti li ottieni gratis con #[derive(...)], altri li scrivi a mano. Questa pagina presenta quelli che incontrerai più spesso e finisce con una tabella da riprendere quando ti serve.
Debug: stampare per chi sviluppa
Sezione intitolata “Debug: stampare per chi sviluppa”Debug ti permette di stampare un valore con {:?}. È pensato per te, che sviluppi: per controllare i valori mentre scrivi e correggi il codice. Non lo scrivi quasi mai a mano; basta derivarlo. Usa {:#?} per una versione «formattata», con un campo per riga, più facile da leggere per le struct grandi:
#[derive(Debug)]struct Book { title: String, pages: u32,}
fn main() { let book = Book { title: String::from("Dune"), pages: 412 }; println!("{:?}", book); println!("{:#?}", book);}Output:
Book { title: "Dune", pages: 412 }Book { title: "Dune", pages: 412,}Anche la macro dbg!, che potresti vedere in altro codice, usa Debug.
Display: stampare per chi usa il programma
Sezione intitolata “Display: stampare per chi usa il programma”Display è quello che usa {}. Serve per il testo rivolto agli utenti: il messaggio che una persona legge sullo schermo. Rust non può indovinare come debba apparire, quindi non esiste un derive. Lo scrivi tu implementando fmt::Display:
use std::fmt;
struct Book { title: String, author: String,}
impl fmt::Display for Book { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{} di {}", self.title, self.author) }}
fn main() { let book = Book { title: String::from("Dune"), author: String::from("Frank Herbert"), };
println!("{}", book);
let text = book.to_string(); println!("Questo testo ha {} caratteri", text.len());}Output:
Dune di Frank HerbertQuesto testo ha 21 caratteriwrite! funziona come println!, ma scrive nel formatter f invece che sullo schermo. Un bel vantaggio: quando un tipo ha Display, ottieni .to_string() gratis. Ritroverai Display nella pagina sugli errori personalizzati, dove dà agli errori un messaggio comprensibile.
Clone e Copy
Sezione intitolata “Clone e Copy”Entrambi creano un duplicato di un valore, ma in modi diversi.
Cloneti dà un metodo.clone(). Lo chiami apposta, e può fare un lavoro vero, come copiare tutto il testo di unaString.Copycrea un duplicato automaticamente ogni volta che assegni o passi il valore. È permesso solo per tipi piccoli e semplici che si possono copiare bit per bit: numeri,bool,chare struct fatte solo di questi.
#[derive(Debug, Clone, Copy)]struct Point { x: i32, y: i32,}
#[derive(Debug, Clone)]struct User { name: String,}
fn main() { let p1 = Point { x: 1, y: 2 }; let p2 = p1; // Copy: p1 si può ancora usare println!("{:?} e {:?}", p1, p2);
let u1 = User { name: String::from("Ana") }; let u2 = u1.clone(); // Clone: chiediamo noi una copia println!("{:?} e {:?}", u1, u2);}Output:
Point { x: 1, y: 2 } e Point { x: 1, y: 2 }User { name: "Ana" } e User { name: "Ana" }Senza Copy, let p2 = p1; sposterebbe p1 e dopo non potresti più usarlo (vedi Ownership). Nota che Copy richiede sempre anche Clone: per questo li derivi insieme.
PartialEq ed Eq: confrontare con ==
Sezione intitolata “PartialEq ed Eq: confrontare con ==”PartialEq ti permette di usare == e !=. Quando lo derivi, due valori sono uguali se tutti i loro campi sono uguali:
#[derive(Debug, PartialEq)]struct Point { x: i32, y: i32,}
fn main() { let a = Point { x: 1, y: 2 }; let b = Point { x: 1, y: 2 }; let c = Point { x: 5, y: 0 };
println!("a == b: {}", a == b); println!("a != c: {}", a != c);}Output:
a == b: truea != c: trueE allora cos’è Eq? Non aggiunge nessun metodo nuovo. È una promessa: «ogni valore è sempre uguale a se stesso». Sembra ovvio, ma i numeri in virgola mobile non la rispettano: in Rust f64::NAN == f64::NAN è false. Per questo f64 ha PartialEq ma non Eq. Ti serve Eq (più Hash) per usare il tuo tipo come chiave di una HashMap o in un HashSet. Se la tua struct non ha campi in virgola mobile, derivali entrambi.
PartialOrd e Ord: confrontare con < e >
Sezione intitolata “PartialOrd e Ord: confrontare con < e >”PartialOrd ti permette di usare <, >, <= e >=. Ord aggiunge un ordinamento completo, che è quello che serve a .sort(), .max() e .min(). Quando li derivi, Rust confronta i campi dall’alto verso il basso: prima il primo campo, e solo se è uguale guarda il successivo.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]struct Version { major: u32, minor: u32, patch: u32,}
fn main() { let old = Version { major: 1, minor: 2, patch: 0 }; let new = Version { major: 1, minor: 10, patch: 3 }; println!("old < new: {}", old < new);
let mut versions = vec![ Version { major: 2, minor: 0, patch: 0 }, new, old, ]; versions.sort();
for v in &versions { println!("{}.{}.{}", v.major, v.minor, v.patch); }}Output:
old < new: true1.2.01.10.32.0.0Siccome i campi vengono confrontati in ordine, conta l’ordine in cui scrivi i campi. Qui major è il più importante, che è proprio quello che vuoi per i numeri di versione. Per le enum, le varianti elencate per prime contano come «più piccole». Come per Eq, i numeri in virgola mobile hanno solo PartialOrd, non Ord: la pagina sull’ordinamento mostra come ordinarli comunque.
Default: un valore iniziale sensato
Sezione intitolata “Default: un valore iniziale sensato”Default dà al tuo tipo una funzione ::default() che costruisce un valore «di partenza». Quando lo derivi, ogni campo riceve il suo valore predefinito: 0 per i numeri, false per bool, una String vuota, un Vec vuoto. La sintassi ..Default::default() ti permette di impostare alcuni campi e riempire gli altri:
#[derive(Debug, Default)]struct Settings { volume: u32, muted: bool, username: String,}
fn main() { let basic = Settings::default(); println!("{:?}", basic);
let custom = Settings { volume: 80, ..Default::default() }; println!("{:?}", custom);}Output:
Settings { volume: 0, muted: false, username: "" }Settings { volume: 80, muted: false, username: "" }Se vuoi valori iniziali diversi (per esempio volume 50), non usare il derive e scrivi tu impl Default for Settings, con una fn default() -> Self che restituisce i valori che vuoi.
From e Into: convertire tra tipi
Sezione intitolata “From e Into: convertire tra tipi”From descrive come costruire un tipo a partire da un altro. L’hai già usato: String::from("ciao"). Quando implementi From, Rust ti regala Into, che va nella direzione opposta:
struct Celsius(f64);struct Fahrenheit(f64);
impl From<Celsius> for Fahrenheit { fn from(c: Celsius) -> Self { Fahrenheit(c.0 * 9.0 / 5.0 + 32.0) }}
fn main() { let boiling = Fahrenheit::from(Celsius(100.0)); println!("L'acqua bolle a {}°F", boiling.0);
let body: Fahrenheit = Celsius(37.0).into(); println!("La temperatura corporea è {:.1}°F", body.0);}Output:
L'acqua bolle a 212°FLa temperatura corporea è 98.6°FCon .into(), Rust deve sapere il tipo di destinazione: per questo abbiamo scritto let body: Fahrenheit.
Come ? usa From. Ecco dove From dà il meglio. Quando l’operatore ? incontra un errore, chiama From::from per trasformarlo nel tipo di errore restituito dalla tua funzione. Quindi, se implementi From per il tuo tipo di errore, ? converte gli errori al posto tuo:
use std::num::ParseIntError;
#[derive(Debug)]enum AppError { BadNumber(ParseIntError),}
impl From<ParseIntError> for AppError { fn from(e: ParseIntError) -> Self { AppError::BadNumber(e) }}
fn parse_age(text: &str) -> Result<u32, AppError> { let age: u32 = text.parse()?; // qui ParseIntError diventa AppError Ok(age)}
fn main() { println!("{:?}", parse_age("42")); println!("{:?}", parse_age("abc"));}Output:
Ok(42)Err(BadNumber(ParseIntError { kind: InvalidDigit }))La pagina sugli errori personalizzati sviluppa questa idea più nel dettaglio.
Sovraccarico degli operatori con Add
Sezione intitolata “Sovraccarico degli operatori con Add”In Rust, a + b è in realtà una chiamata al metodo add del trait std::ops::Add. Implementalo per il tuo tipo e + funziona e basta. type Output dice di che tipo è il risultato della somma:
use std::ops::Add;
#[derive(Debug, Clone, Copy, PartialEq)]struct Point { x: i32, y: i32,}
impl Add for Point { type Output = Point;
fn add(self, other: Point) -> Point { Point { x: self.x + other.x, y: self.y + other.y } }}
fn main() { let a = Point { x: 1, y: 2 }; let b = Point { x: 3, y: 4 }; println!("{:?}", a + b);}Output:
Point { x: 4, y: 6 }La stessa idea vale per Sub (-), Mul (*), Neg (il - unario) e AddAssign (+=). Usala quando il significato è ovvio: sommare due punti o due importi di denaro. Se chi legge dovesse indovinare cosa vuol dire + per il tuo tipo, meglio un metodo normale con un nome chiaro.
Quali trait dovrei derivare?
Sezione intitolata “Quali trait dovrei derivare?”Ecco una guida veloce per una struct tipica:
| Trait | Derivarlo? | Cosa ottieni |
|---|---|---|
Debug |
Quasi sempre | Stampa con {:?} e {:#?} |
Clone |
Di solito | .clone() |
Copy |
Solo per tipi piccoli senza campi String, Vec o Box |
Copie automatiche invece di spostamenti |
PartialEq |
Di solito | == e != |
Eq |
Quando non ci sono campi in virgola mobile | Serve per le chiavi di una HashMap (con Hash) |
Hash |
Quando lo usi come chiave di una HashMap o in un HashSet |
Hashing |
PartialOrd, Ord |
Quando i valori hanno un ordine naturale | <, >, .sort(), .max() |
Default |
Quando un valore «vuoto» ha senso | ::default() e ..Default::default() |
Display |
Non si può derivare: scrivilo a mano | Stampa con {} e .to_string() |
Una riga di partenza molto comune è #[derive(Debug, Clone, PartialEq)]. Aggiungine altri quando ti servono: il compilatore ti dirà quando ne manca uno.
Errori comuni
Sezione intitolata “Errori comuni”Derivare Copy su una struct con una String.
#[derive(Clone, Copy)]struct User { name: String, age: u32,}error[E0204]: the trait `Copy` cannot be implemented for this type --> src/main.rs:2:8 |1 | #[derive(Clone, Copy)] | ---- in this derive macro expansion2 | struct User { | ^^^^3 | name: String, | ------------ this field does not implement `Copy`Il compilatore dice (in inglese) che il trait Copy non si può implementare per questo tipo, perché il campo name non implementa Copy. Una String possiede memoria nello heap, quindi non si può copiare bit per bit. Togli Copy e usa .clone() quando ti serve un duplicato.
Derivare Eq su una struct con un numero in virgola mobile.
#[derive(Debug, PartialEq, Eq)]struct Product { name: String, price: f64 }error[E0277]: the trait bound `f64: Eq` is not satisfied --> src/main.rs:2:32 |1 | #[derive(Debug, PartialEq, Eq)] | -- in this derive macro expansion2 | struct Product { name: String, price: f64 } | ^^^^^^^^^^ the trait `Eq` is not implemented for `f64`Qui il messaggio dice che f64 non implementa Eq. Tieni PartialEq e togli Eq. Oppure salva il prezzo come centesimi interi in un u64, che è anche più sicuro per il denaro.
Pratica
Sezione intitolata “Pratica”1. Ordina una lista di cose da fare
Sezione intitolata “1. Ordina una lista di cose da fare”Crea una struct Task con un priority: u8 e un title: String. Deriva i trait che ti servono per poter ordinare un Vec<Task>, stampare ogni attività con {:?} e confrontare due attività con ==. I numeri più bassi sono più urgenti, quindi devono venire prima. Suggerimento: .sort() ha bisogno di Ord, e Ord ha bisogno anche di Eq, PartialEq e PartialOrd. Metti priority per primo, così viene confrontato per primo.
Mostra la soluzione
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]struct Task { priority: u8, title: String,}
fn main() { let mut tasks = vec![ Task { priority: 3, title: String::from("Annaffiare le piante") }, Task { priority: 1, title: String::from("Pagare l'affitto") }, Task { priority: 2, title: String::from("Rispondere a Sam") }, ];
let copy = tasks[1].clone(); println!("Stessa attività? {}", copy == tasks[1]);
tasks.sort(); for task in &tasks { println!("{:?}", task); }}Output:
Stessa attività? trueTask { priority: 1, title: "Pagare l'affitto" }Task { priority: 2, title: "Rispondere a Sam" }Task { priority: 3, title: "Annaffiare le piante" }2. Denaro che si stampa e si somma
Sezione intitolata “2. Denaro che si stampa e si somma”Crea una struct Money che salva i centesimi interi in cents: u64. Implementa From<u64> così che 350.into() funzioni, Display così che si stampi come 3,50 €, e Add così che due importi si possano sommare con +. Suggerimento: gli euro sono cents / 100 e il resto è cents % 100. Stampa il resto con {:02}, così 5 centesimi diventano 05. Deriva Copy per poter ancora stampare gli importi dopo averli sommati.
Mostra la soluzione
use std::fmt;use std::ops::Add;
#[derive(Debug, Clone, Copy, PartialEq)]struct Money { cents: u64,}
impl From<u64> for Money { fn from(cents: u64) -> Self { Money { cents } }}
impl fmt::Display for Money { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{},{:02} €", self.cents / 100, self.cents % 100) }}
impl Add for Money { type Output = Money;
fn add(self, other: Money) -> Money { Money { cents: self.cents + other.cents } }}
fn main() { let coffee: Money = 350.into(); let cake = Money::from(405); let total = coffee + cake;
println!("Caffè: {coffee}"); println!("Torta: {cake}"); println!("Totale: {total}");}Output:
Caffè: 3,50 €Torta: 4,05 €Totale: 7,55 €Riepilogo
Sezione intitolata “Riepilogo”Debug({:?}) è per chi sviluppa e si deriva quasi sempre.Display({}) è per gli utenti e si scrive a mano.Clonecopia su richiesta con.clone().Copycopia automaticamente, ma solo per i tipi semplici.PartialEq/Eqti danno==.PartialOrd/Ordti danno<e l’ordinamento, confrontando i campi dall’alto verso il basso.Defaultcostruisce un valore di partenza;..Default::default()riempie i campi che non hai impostato.- Implementa
Frome ottieniIntogratis. L’operatore?usaFromper convertire gli errori. - Implementa
std::ops::Add(e i suoi fratelli) per far funzionare+con i tuoi tipi.
Vuoi andare oltre? I trait avanzati spiegano i tipi associati come type Output, i parametri di tipo predefiniti e come implementare un trait per un tipo che non hai scritto tu.
Verifica veloce
Sezione intitolata “Verifica veloce”Verifica veloce
1. Quale trait usa {} in println!?
2. Perché una struct con un campo String non può derivare Copy?
3. Quando derivi PartialOrd, come vengono confrontate due struct?
4. Hai implementato From<ParseIntError> for AppError. Cosa permette di fare a ??
5. Quale trait implementi per far funzionare a + b con un tuo tipo?
Punteggio: 0 / 5
Per approfondire
Sezione intitolata “Per approfondire”- Derivable Traits — The Rust Book (Appendix C) (in inglese): tutti i trait che puoi usare con
#[derive], cosa fa la versione derivata e quando ti conviene. std::fmt— formattazione e Display (in inglese): tutte le opzioni di formattazione, come larghezza, riempimento e{:#?}, e il ruolo diDisplayeDebug.FromandInto— Rust by Example (in inglese): brevi esempi eseguibili di conversioni tra tipi tuoi.std::ops— trait degli operatori (in inglese): l’elenco completo degli operatori che puoi sovraccaricare, daAddaIndex.std::cmp— trait di confronto (in inglese):PartialEq,Eq,PartialOrdeOrdnel dettaglio, con le regole che ciascuno promette.