Lifetime in Rust
I lifetime (tempi di vita) sono il meccanismo con cui Rust tiene traccia di quanto a lungo i riferimenti restano validi. Nella maggior parte dei casi, i lifetime vengono inferiti automaticamente dal compilatore. Quando ciò non è possibile, è necessario annotarli esplicitamente. I lifetime non cambiano la durata dei riferimenti: descrivono semplicemente le relazioni tra i tempi di vita di più riferimenti.
Cosa sono i lifetime
Ogni riferimento in Rust ha un lifetime, cioè lo scope entro il quale il riferimento è valido. Di solito il compilatore lo determina da solo:
fn main() {
let r; // r dichiarata senza valore
{
let x = 5;
r = &x; // r prende un riferimento a x
} // x esce dallo scope, il riferimento diventa invalido
// println!("{r}"); // Errore! r è un dangling reference
}
Il compilatore rifiuta questo codice perche r vivrebbe più a lungo di x, creando un riferimento pendente. Il lifetime di &x è limitato al blocco interno.
Annotazioni di lifetime ('a)
Le annotazioni di lifetime usano la sintassi 'a (apostrofo seguito da un nome, per convenzione lettere minuscole brevi). Non cambiano la durata del riferimento: descrivono la relazione tra i lifetime di più riferimenti.
fn piu_lungo<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() >= s2.len() {
s1
} else {
s2
}
}
fn main() {
let stringa1 = String::from("lunga stringa");
{
let stringa2 = String::from("xyz");
let risultato = piu_lungo(&stringa1, &stringa2);
println!("La più lunga è: {risultato}");
}
}
L’annotazione 'a dice al compilatore: “il riferimento restituito vivrà almeno quanto il più corto tra i lifetime di s1 e s2”.
Lifetime nelle firme delle funzioni
Quando una funzione restituisce un riferimento, il compilatore deve sapere a quale parametro di input è legato il lifetime del risultato.
// Il risultato è legato al lifetime di entrambi i parametri
fn primo_non_vuoto<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if !s1.is_empty() {
s1
} else {
s2
}
}
// Lifetime diversi quando il risultato dipende solo da un parametro
fn primo_carattere<'a>(s: &'a str, _prefisso: &str) -> &'a str {
&s[0..1]
}
fn main() {
let a = String::from("ciao");
let b = String::from("mondo");
println!("{}", primo_non_vuoto(&a, &b));
println!("{}", primo_carattere(&a, ">>"));
}
Regole di elisione dei lifetime
Rust ha tre regole di lifetime elision che permettono di omettere le annotazioni nei casi più comuni:
-
Ogni parametro riferimento riceve il proprio lifetime:
fn f(x: &str, y: &str)diventafn f<'a, 'b>(x: &'a str, y: &'b str). -
Se c’è esattamente un parametro riferimento, il suo lifetime viene assegnato a tutti i riferimenti in output:
fn f(x: &str) -> &strdiventafn f<'a>(x: &'a str) -> &'a str. -
Se uno dei parametri è
&selfo&mut self, il lifetime diselfviene assegnato a tutti i riferimenti in output.
// Regola 2: un solo parametro riferimento, il lifetime viene inferito
fn primo(s: &str) -> &str {
&s[..1]
}
// Equivalente a:
// fn primo<'a>(s: &'a str) -> &'a str
// Regola 3: metodo con &self
struct Testo {
contenuto: String,
}
impl Testo {
fn estratto(&self) -> &str {
&self.contenuto
}
// Equivalente a:
// fn estratto<'a>(&'a self) -> &'a str
}
fn main() {
let t = Testo { contenuto: String::from("Ciao Rust") };
println!("{}", primo("hello"));
println!("{}", t.estratto());
}
Quando le regole di elisione non sono sufficienti, il compilatore richiede annotazioni esplicite.
Lifetime nelle struct
Quando una struct contiene un riferimento, è necessario annotare il lifetime per garantire che il riferimento non sopravviva al dato a cui punta.
struct Estratto<'a> {
testo: &'a str,
}
impl<'a> Estratto<'a> {
fn nuovo(testo: &'a str) -> Self {
Estratto { testo }
}
fn lunghezza(&self) -> usize {
self.testo.len()
}
}
fn main() {
let romanzo = String::from("Era una notte buia e tempestosa");
let estratto = Estratto::nuovo(&romanzo);
println!("Estratto: '{}' ({} caratteri)", estratto.testo, estratto.lunghezza());
}
Il lifetime 'a garantisce che l’istanza di Estratto non possa vivere più a lungo della stringa a cui fa riferimento.
Struct con più lifetime:
struct Coppia<'a, 'b> {
primo: &'a str,
secondo: &'b str,
}
fn main() {
let s1 = String::from("Ciao");
let coppia;
{
let s2 = String::from("Mondo");
coppia = Coppia { primo: &s1, secondo: &s2 };
println!("{} {}", coppia.primo, coppia.secondo);
}
// coppia non è più utilizzabile qui perché s2 è stata deallocata
}
Il lifetime statico ('static)
Il lifetime 'static indica che un riferimento può vivere per l’intera durata del programma. Le stringhe letterali hanno lifetime 'static perche sono incorporate nel binario.
fn main() {
let s: &'static str = "Questa stringa vive per sempre";
println!("{s}");
}
'static è usato anche come vincolo sui generics:
fn stampa_statico(s: &'static str) {
println!("{s}");
}
fn main() {
stampa_statico("Ciao"); // OK: stringhe letterali sono 'static
}
Attenzione: usare 'static come soluzione ai problemi di lifetime è quasi sempre sbagliato. Preferisci lifetime generici.
Lifetime bounds sui generics
I vincoli di lifetime possono essere applicati ai parametri generici per esprimere relazioni tra tipi e riferimenti.
fn stampa_riferimento<'a, T>(t: &'a T)
where
T: std::fmt::Display + 'a,
{
println!("{t}");
}
fn piu_lungo_generico<'a, T>(x: &'a T, y: &'a T) -> &'a T
where
T: PartialOrd,
{
if x >= y { x } else { y }
}
fn main() {
let a = 10;
let b = 20;
let max = piu_lungo_generico(&a, &b);
println!("Il maggiore è: {max}");
stampa_riferimento(&String::from("test"));
}
Il vincolo T: 'a significa che tutti i riferimenti contenuti in T devono vivere almeno quanto 'a.
Esempio completo
struct Configurazione<'a> {
nome: &'a str,
versione: &'a str,
}
impl<'a> Configurazione<'a> {
fn descrizione(&self) -> String {
format!("{} v{}", self.nome, self.versione)
}
fn nome_o_default(&self, default: &str) -> &'a str {
if self.nome.is_empty() {
// Non possiamo restituire 'default' qui perché ha un lifetime diverso
// restituiamo il nome (anche se vuoto) che ha lifetime 'a
self.nome
} else {
self.nome
}
}
}
fn main() {
let nome = String::from("MiaApp");
let versione = String::from("1.0");
let config = Configurazione {
nome: &nome,
versione: &versione,
};
println!("{}", config.descrizione());
println!("Nome: {}", config.nome_o_default("Sconosciuto"));
}
Conclusione
I lifetime sono il terzo pilastro della sicurezza della memoria in Rust, insieme all’ownership e al borrowing. Nella pratica quotidiana, le regole di elisione gestiscono la maggior parte dei casi automaticamente. Quando servono annotazioni esplicite, ricorda che i lifetime non cambiano la durata dei riferimenti: descrivono le relazioni tra di essi affinché il compilatore possa verificare che tutto sia corretto. Con il tempo e la pratica, le annotazioni di lifetime diventeranno naturali.