Lifetime in Rust
I lifetime (durata) sono di solito il punto in cui chi inizia con Rust incontra il primo vero muro. Questa pagina procede con calma: l’obiettivo è solo riconoscere cos’è un lifetime e perché il compilatore ne chiede uno, non padroneggiare ogni regola.
Il problema che i lifetime risolvono
Sezione intitolata “Il problema che i lifetime risolvono”Dalla pagina sul prestito sai già che un riferimento (&) ti permette di usare un valore senza possederlo. Ma un riferimento è valido solo finché esiste il valore a cui punta. Rust deve assicurarsi che tu non finisca mai con un riferimento a qualcosa che è già stato ripulito: questo tipo di bug si chiama riferimento pendente (dangling reference) ed è una fonte classica di crash in linguaggi come il C.
Quasi sempre Rust ci arriva da solo guardando gli scope del tuo codice, e tu non devi pensarci (è proprio quello che hai visto nella pagina sullo scope). A volte però, soprattutto con le funzioni, il compilatore non riesce a capirlo da solo e ti chiede di scriverlo in modo esplicito. Ecco cos’è un’annotazione di lifetime.
Una funzione in cui Rust ha bisogno di aiuto
Sezione intitolata “Una funzione in cui Rust ha bisogno di aiuto”Ecco una funzione che restituisce la più lunga tra due slice di stringa:
fn longest(a: &str, b: &str) -> &str { if a.len() > b.len() { a } else { b }}Non compila. L’errore è questo:
error[E0106]: missing lifetime specifierIl compilatore ti dice (in inglese) che manca l’indicazione di un lifetime. Il problema: il riferimento restituito potrebbe venire da a oppure da b, e Rust non può capire quale leggendo solo la funzione. Quindi non può garantire che il riferimento che ricevi sia ancora valido quando lo usi.
Aggiungere un’annotazione di lifetime
Sezione intitolata “Aggiungere un’annotazione di lifetime”Lo correggi dando un nome a un lifetime (per convenzione una lettera minuscola come 'a) e dicendo a Rust che il lifetime del valore restituito è legato a entrambi i parametri:
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str { if a.len() > b.len() { a } else { b }}
fn main() { let s1 = String::from("buongiorno"); let s2 = String::from("ciao");
let result = longest(&s1, &s2); println!("La più lunga: {result}");}Output:
La più lunga: buongiornoLeggi 'a come un’etichetta, non come un valore: non fissa una durata reale. Dice solo a Rust «tutti questi riferimenti devono restare validi almeno per lo stesso tratto di codice». Poi il compilatore controlla ogni chiamata per verificare che sia davvero così.
Perché è importante
Sezione intitolata “Perché è importante”Senza questa annotazione, il compilatore non può davvero sapere se il riferimento restituito da longest potrebbe vivere più a lungo di una delle stringhe da cui viene. L’annotazione di lifetime è ciò che gli permette di dimostrare, in fase di compilazione e senza alcun costo a runtime, che il riferimento che ricevi sarà sempre valido.
Struct che contengono riferimenti
Sezione intitolata “Struct che contengono riferimenti”Il prossimo posto in cui incontrerai i lifetime è una struct. Finora ogni campo di una struct possedeva i suoi dati, come una String. Un campo può anche essere un riferimento, ma allora la struct è valida solo finché lo sono i dati a cui punta. Rust ti fa scrivere questo legame con un lifetime:
struct Highlight<'a> { text: &'a str, // prende in prestito da una String che vive altrove}
fn main() { let book = String::from("Call me Ishmael. Some years ago..."); let first = Highlight { text: &book[..15] };
println!("Evidenziato: {}", first.text); println!("Il libro intero è lungo {} byte", book.len());}Output:
Evidenziato: Call me IshmaelIl libro intero è lungo 34 byteLeggi Highlight<'a> come «un Highlight che prende in prestito qualcosa per il lifetime 'a». La parte &book[..15] è una slice: prende in prestito i primi 15 byte di book senza copiarli. Siccome first prende solo in prestito, book si può ancora usare dopo. (Il testo del libro, l’inizio di Moby Dick, resta in inglese perché la slice conta i byte: con lettere accentate i numeri cambierebbero.)
Il lifetime è ciò che permette a Rust di scoprire questo errore, in cui la String viene rilasciata mentre la struct punta ancora al suo interno:
struct Highlight<'a> { text: &'a str,}
fn main() { let highlight; { let book = String::from("Call me Ishmael. Some years ago..."); highlight = Highlight { text: &book[..15] }; } // book is dropped here...
println!("{}", highlight.text); // ...but highlight still points into it}error[E0597]: `book` does not live long enough --> src/main.rs:9:40 | 8 | let book = String::from("Call me Ishmael. Some years ago..."); | ---- binding `book` declared here 9 | highlight = Highlight { text: &book[..15] }; | ^^^^ borrowed value does not live long enough10 | } // book is dropped here... | - `book` dropped here while still borrowed11 |12 | println!("{}", highlight.text); // ...but highlight still points into it | -------------- borrow later used hereIl compilatore spiega che book non vive abbastanza a lungo: viene rilasciata alla fine del blocco interno (i commenti nel codice lo dicono: «qui book viene rilasciata…», «…ma highlight punta ancora al suo interno»), mentre highlight la usa più tardi.
Un consiglio pratico: se i lifetime in una struct ti danno fastidio, salva una String posseduta invece di un &str. Il testo viene copiato, ma la struct non dipende più da nient’altro e il lifetime sparisce. Molti programmatori Rust esperti partono così e passano ai riferimenti solo quando hanno un buon motivo.
La buona notizia
Sezione intitolata “La buona notizia”Scriverai 'a molto meno spesso di quanto questa pagina possa far pensare. Tantissimo codice Rust non ha mai bisogno di un lifetime esplicito, perché il compilatore sa dedurli nella maggior parte delle situazioni di tutti i giorni. Li incontri soprattutto quando una funzione restituisce un riferimento e il compilatore non riesce a capire da solo da dove viene. A quel punto ricorda: non è un bug, è il compilatore che ti chiede di confermare qualcosa che non può dimostrare da solo.
Pratica
Sezione intitolata “Pratica”1. La stringa più corta
Sezione intitolata “1. La stringa più corta”Scrivi una funzione shortest che prende due slice di stringa e restituisce la più corta. Se hanno la stessa lunghezza, restituisce la prima. Chiamala con "elefante" e "gatto".
Suggerimento: ha la stessa forma di longest qui sopra. Ti serve <'a> dopo il nome, e 'a su entrambi i parametri e sul tipo di ritorno.
Mostra la soluzione
fn shortest<'a>(a: &'a str, b: &'a str) -> &'a str { if a.len() <= b.len() { a } else { b }}
fn main() { let big = String::from("elefante"); let small = String::from("gatto");
println!("La più corta: {}", shortest(&big, &small)); println!("A parità vince la prima: {}", shortest("cane", "orso"));}Output:
La più corta: gattoA parità vince la prima: caneLa slice restituita potrebbe venire da a o da b, quindi Rust ha bisogno di 'a per sapere che il risultato è valido solo finché lo sono entrambi gli input. Usare <= invece di < fa sì che, a parità, venga scelta a.
2. Una struct che prende in prestito
Sezione intitolata “2. Una struct che prende in prestito”Crea una struct Quote con due campi &str, text e author. Creane una in cui text prende in prestito i primi 20 byte della String "Fearless concurrency is a goal of Rust" e author è il letterale "The Rust Book". Stampala come Fearless concurrency (The Rust Book).
Suggerimento: come Highlight qui sopra, la struct ha bisogno di un lifetime: struct Quote<'a>, con &'a str per entrambi i campi. &speech[..20] è una slice dei primi 20 byte.
Mostra la soluzione
struct Quote<'a> { text: &'a str, author: &'a str,}
fn main() { let speech = String::from("Fearless concurrency is a goal of Rust"); let quote = Quote { text: &speech[..20], author: "The Rust Book", };
println!("{} ({})", quote.text, quote.author);}Output:
Fearless concurrency (The Rust Book)quote prende in prestito da speech, quindi non può vivere più a lungo di lei. Il letterale "The Rust Book" è incorporato nel programma e vive per tutta l’esecuzione, quindi dura senz’altro abbastanza per 'a. Prova a creare speech dentro un blocco { } interno e a stampare la citazione dopo la fine del blocco, come nell’esempio di errore qui sopra: vedrai il compilatore fermarti.
Riepilogo
Sezione intitolata “Riepilogo”- Un lifetime dice al compilatore come i riferimenti sono legati tra loro, così può escludere i riferimenti pendenti.
- Devi scriverne uno solo quando il compilatore non riesce a dedurlo da solo: il caso più comune sono le funzioni che restituiscono un riferimento.
'anon fissa una durata: collega i lifetime di riferimenti diversi così il compilatore può controllarli.- Anche una struct che contiene un riferimento ha bisogno di un lifetime (
struct Highlight<'a>) e non può vivere più a lungo dei dati che prende in prestito. Salvare unaStringposseduta evita il problema. - È tutto un controllo in fase di compilazione: a runtime non costa nulla.
Verifica veloce
Sezione intitolata “Verifica veloce”Verifica veloce
1. Quale problema risolvono i lifetime?
2. Perché fn longest(a: &str, b: &str) -> &str non compilava senza un'annotazione di lifetime?
3. Aggiungere un'annotazione di lifetime costa qualcosa a runtime?
4. Secondo questa pagina, quando ti capita più spesso di dover scrivere un lifetime esplicito?
Punteggio: 0 / 4
Per approfondire
Sezione intitolata “Per approfondire”- Validating References with Lifetimes — The Rust Book (in inglese): la spiegazione ufficiale completa, con i lifetime nelle struct, le regole di elisione e
'static. - Functions — Rust by Example (in inglese): brevi esempi di funzioni con annotazioni di lifetime, e quali il compilatore rifiuta.
- Structs — Rust by Example (in inglese): altre struct ed enum che contengono riferimenti.
- Lifetime elision — The Rust Reference (in inglese): le regole esatte con cui il compilatore inserisce i lifetime al posto tuo, così sai quando puoi ometterli.