Rust Lifetimes: Annotazioni, Elision e Domande da Colloquio 2026

Una guida completa ai lifetimes in Rust: dai concetti fondamentali alle annotazioni, le regole di elision e le domande più frequenti nei colloqui tecnici.

Rust Lifetimes annotazioni e visualizzazione del borrow checker

I lifetimes in Rust rappresentano il meccanismo attraverso cui il compilatore tiene traccia della validità dei riferimenti. A differenza dei linguaggi con garbage collection, dove la gestione della memoria avviene a runtime, il borrow checker di Rust valida la validità dei riferimenti a tempo di compilazione, eliminando intere categorie di bug di memoria prima ancora che il codice venga eseguito.

Risposta Rapida per i Colloqui

Un lifetime in Rust è un costrutto a tempo di compilazione che descrive l'ambito durante il quale un riferimento rimane valido. Il borrow checker utilizza i lifetimes per garantire che i riferimenti non sopravvivano mai ai dati a cui puntano, prevenendo i dangling pointer senza overhead a runtime.

Cosa rappresentano realmente i Lifetimes nella memoria

I lifetimes non riguardano quanto a lungo esistono i valori. Descrivono quanto a lungo i riferimenti ai valori rimangono validi. Ogni riferimento in Rust ha un lifetime, anche quando le annotazioni vengono omesse.

Consideriamo questa funzione che il compilatore rifiuta:

dangling_reference.rsrust
fn create_dangling() -> &String {
    let s = String::from("hello");
    &s  // ERROR: `s` is dropped at end of function
}

La String s vive solo all'interno dello scope della funzione. Restituire un riferimento ad essa creerebbe un dangling pointer, poiché la memoria della String viene deallocata quando la funzione termina. Il borrow checker intercetta questo errore a tempo di compilazione.

La soluzione richiede di restituire un valore owned oppure di garantire che i dati referenziati sopravvivano alla chiamata della funzione:

valid_return.rsrust
// Option 1: Return owned value
fn create_owned() -> String {
    String::from("hello")
}

// Option 2: Reference data that outlives the function
fn first_word(s: &str) -> &str {
    s.split_whitespace().next().unwrap_or("")
}

In first_word, il &str restituito prende in prestito dall'input s, quindi rimane valido finché s esiste. Il chiamante controlla il lifetime dell'input.

Sintassi e semantica delle annotazioni di Lifetime

Le annotazioni esplicite di lifetime utilizzano la sintassi 'a, 'b, e così via. Queste non sono istruzioni al compilatore su quanto a lungo i riferimenti debbano vivere. Descrivono le relazioni tra i lifetimes di più riferimenti.

La Rust Reference definisce le annotazioni di lifetime come parametri generici che vincolano quanto a lungo i riferimenti devono rimanere validi l'uno rispetto all'altro.

lifetime_annotations.rsrust
// Both inputs and output share the same lifetime 'a
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let string1 = String::from("long string");
    let result;
    {
        let string2 = String::from("short");
        result = longest(&string1, &string2);
        println!("Longest: {}", result);  // Valid: both strings alive
    }
    // println!("{}", result);  // ERROR: string2 dropped
}

L'annotazione 'a indica al compilatore: il riferimento restituito sarà valido per l'intersezione dei lifetimes di x e y. Poiché string2 ha un lifetime più breve, result non può essere utilizzato dopo che string2 viene eliminato.

Lifetimes multipli e distinti esprimono relazioni più complesse:

multiple_lifetimes.rsrust
// Output tied only to first parameter's lifetime
fn first_only<'a, 'b>(x: &'a str, _y: &'b str) -> &'a str {
    x
}

fn main() {
    let owned = String::from("owned");
    let result;
    {
        let temporary = String::from("temporary");
        result = first_only(&owned, &temporary);
    }
    // result still valid: only depends on owned
    println!("{}", result);
}

Regole di Lifetime Elision in Rust 2024

Il compilatore applica tre regole di elision per inferire i lifetimes quando le annotazioni vengono omesse. Queste regole, documentate nel Rustonomicon, riducono il boilerplate senza compromettere la sicurezza.

Le tre regole di Elision
  1. Ogni riferimento in input ottiene il proprio parametro di lifetime
  2. Se esiste esattamente un lifetime in input, si applica a tutti i riferimenti in output
  3. Se esiste &self o &mut self, il suo lifetime si applica a tutti i riferimenti in output

Queste regole coprono la maggior parte dei pattern comuni:

elision_examples.rsrust
// Rule 1: Each input gets own lifetime
fn takes_two(x: &str, y: &str) {}
// Compiler reads: fn takes_two<'a, 'b>(x: &'a str, y: &'b str) {}

// Rule 2: Single input lifetime propagates to output  
fn first_char(s: &str) -> &str {
    &s[0..1]
}
// Compiler reads: fn first_char<'a>(s: &'a str) -> &'a str

// Rule 3: &self lifetime propagates to output
impl Parser {
    fn peek(&self) -> &Token {
        &self.tokens[self.position]
    }
    // Compiler reads: fn peek<'a>(&'a self) -> &'a Token
}

Quando l'elision fallisce, il compilatore richiede annotazioni esplicite. Questo accade più frequentemente con funzioni che restituiscono riferimenti derivati da più input:

elision_fails.rsrust
// ERROR: Can't determine output lifetime
fn ambiguous(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}

// FIX: Explicit annotation resolves ambiguity
fn unambiguous<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

Lifetimes nelle Struct e la relazione Outlives

Le struct contenenti riferimenti richiedono annotazioni di lifetime per esprimere che i dati presi in prestito devono sopravvivere alla struct:

struct_lifetimes.rsrust
struct Excerpt<'a> {
    text: &'a str,
}

impl<'a> Excerpt<'a> {
    // new borrows from input, so Excerpt can't outlive the source
    fn new(source: &'a str, start: usize, end: usize) -> Self {
        Excerpt { text: &source[start..end] }
    }
    
    // level() returns owned data, no lifetime in signature
    fn level(&self) -> u32 {
        self.text.len() as u32 / 10
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let excerpt = Excerpt::new(&novel, 0, 16);
    println!("Excerpt: {}", excerpt.text);
}  // novel dropped, but excerpt already out of scope

Il 'a in Excerpt<'a> lega la validità della struct al lifetime del text preso in prestito. Tentare di utilizzare un Excerpt dopo che la sua String sorgente viene eliminata genera un errore di compilazione.

Per struct con più riferimenti, ciascuno può avere lifetimes distinti:

multiple_struct_lifetimes.rsrust
struct Comparison<'a, 'b> {
    baseline: &'a str,
    candidate: &'b str,
}

impl<'a, 'b> Comparison<'a, 'b> {
    fn new(baseline: &'a str, candidate: &'b str) -> Self {
        Comparison { baseline, candidate }
    }
    
    fn baseline_only(&self) -> &'a str {
        self.baseline
    }
}

Pronto a superare i tuoi colloqui su Rust?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Il Lifetime 'static e quando utilizzarlo

Il lifetime 'static indica che i dati vivono per l'intera esecuzione del programma. I literal di stringa hanno questo lifetime perché sono incorporati nel binario:

static_lifetime.rsrust
let s: &'static str = "I live forever";

// Constants are implicitly 'static
const CONFIG_VERSION: &str = "2.0.0";

// Thread-safe globals require 'static
static COUNTER: AtomicU64 = AtomicU64::new(0);

Un pattern comune coinvolge i bound T: 'static, che spesso confonde gli sviluppatori. Questo bound significa che T non contiene riferimenti non-statici, non che T debba essere un riferimento:

static_bound.rsrust
use std::thread;

fn spawn_task<T: Send + 'static>(data: T) {
    thread::spawn(move || {
        // data moved into thread, must live independently
        println!("Processing in thread");
    });
}

fn main() {
    let owned = String::from("owned data");
    spawn_task(owned);  // OK: String is 'static (owns its data)
    
    let reference = "borrowed";
    // spawn_task(reference);  // OK: &'static str
}

Il thread potrebbe sopravvivere alla funzione che lo ha generato, quindi i dati spostati nei thread non devono contenere riferimenti a variabili locali dello stack.

Errori comuni sui Lifetimes e relative soluzioni

Diversi pattern causano regolarmente problemi agli sviluppatori. Comprendere questi accelera il debugging.

Restituire riferimenti a variabili locali:

error_local_ref.rsrust
fn bad_split(text: &str, delimiter: char) -> (&str, &str) {
    let parts: Vec<&str> = text.split(delimiter).collect();
    (parts[0], parts[1])  // OK: parts[i] borrow from text, not from Vec
}

fn truly_bad() -> &str {
    let local = String::from("local");
    &local  // ERROR: local dropped at end of function
}

Memorizzare riferimenti in struct con lifetimes non corrispondenti:

error_struct_lifetime.rsrust
struct Cache {
    data: String,
    // view: &str,  // ERROR: needs lifetime parameter
}

// Self-referential structs require unsafe or crates like ouroboros
struct CacheWithView<'a> {
    data: String,
    view: Option<&'a str>,  // Can't point to self.data safely
}

Le struct autoreferenziali, dove un campo referenzia un altro campo, richiedono Pin con codice unsafe oppure crate come ouroboros. Il modulo Rust Smart Pointers tratta questi pattern avanzati.

Bound di lifetime nelle implementazioni di trait:

trait_lifetime_bounds.rsrust
trait Processor {
    fn process<'a>(&self, input: &'a str) -> &'a str;
}

struct Prefixer {
    prefix: String,
}

impl Processor for Prefixer {
    // Can't return &format!(...) - would be temporary
    fn process<'a>(&self, input: &'a str) -> &'a str {
        input  // Must return input or part of it
    }
}

Higher-Ranked Trait Bounds (HRTBs) per Lifetimes generici

I Higher-Ranked Trait Bounds utilizzano la sintassi for<'a> per esprimere che un tipo deve soddisfare un trait per qualsiasi lifetime, non solo per uno specifico:

hrtb.rsrust
use std::fmt::Debug;

// F must be callable with any lifetime 'a
fn apply_to_refs<F>(f: F)
where
    F: for<'a> Fn(&'a str) -> &'a str,
{
    let owned = String::from("test");
    let result = f(&owned);
    println!("{}", result);
}

fn identity(s: &str) -> &str { s }

fn main() {
    apply_to_refs(identity);
}

Gli HRTBs appaiono frequentemente nelle API che accettano closure e nell'ecosistema Rust async/await dove i futures devono funzionare con riferimenti di lifetimes variabili.

Domande da colloquio sui Lifetimes in Rust

I colloqui tecnici sondano la comprensione dei lifetimes a più livelli di profondità. Queste domande appaiono regolarmente nelle posizioni Rust.

Domanda: Perché questo codice non compila?

interview_q1.rsrust
fn get_str() -> &str {
    "hello"
}

Risposta: Il tipo di ritorno necessita di un'annotazione esplicita di lifetime. Sebbene il literal di stringa abbia lifetime 'static, la firma della funzione non lo esprime. La correzione: fn get_str() -> &'static str.

Domanda: Spiega perché questo compila e se è sicuro:

interview_q2.rsrust
fn longest<'a>(x: &'a str, _y: &str) -> &'a str {
    x
}

Risposta: Questo compila perché il lifetime dell'output è legato solo a x. Il parametro _y può avere qualsiasi lifetime poiché il valore di ritorno non dipende da esso. Questo è sicuro: la validità del riferimento restituito dipende esclusivamente dal lifetime di x.

Domanda: Cosa succede quando si cerca di memorizzare un riferimento insieme a dati owned?

interview_q3.rsrust
struct Config<'a> {
    name: String,
    description: &'a str,
}

Risposta: Questo pattern è valido ma vincola come Config può essere utilizzato. La struct non può sopravvivere a ciò a cui description fa riferimento. Per dati owned che devono referenziare se stessi, considerare l'uso di String per entrambi i campi, oppure le tecniche trattate in Rust ownership e borrowing.

Domanda: Come interagiscono i lifetimes con i trait objects?

interview_q4.rsrust
trait Formatter {
    fn format(&self, input: &str) -> String;
}

fn get_formatter<'a>() -> Box<dyn Formatter + 'a> {
    // ...
}

Risposta: I trait objects hanno un bound di lifetime 'static implicito per default. Scrivere Box<dyn Formatter + 'a> permette esplicitamente al trait object di contenere riferimenti con lifetime 'a. Senza il bound esplicito, Box<dyn Formatter> equivale a Box<dyn Formatter + 'static>.

Varianza dei Lifetimes: Covarianza e Controvarianza

La varianza determina come i lifetimes si relazionano quando i tipi sono annidati. I riferimenti in Rust seguono queste regole:

  • &'a T è covariante in 'a: un lifetime più lungo può sostituire uno più breve
  • &'a mut T è invariante in T: il tipo deve corrispondere esattamente
  • fn(&'a T) è controvariante in 'a: un lifetime più breve può sostituire uno più lungo
variance.rsrust
fn covariant_example() {
    let s: &'static str = "static";
    let r: &str = s;  // OK: 'static lives longer than any 'a
}

fn invariant_example() {
    let mut vec: Vec<&'static str> = vec!["a"];
    // let s = String::from("local");
    // vec.push(&s);  // ERROR: &s is not &'static str
}

Comprendere la varianza è importante quando si progettano API generiche che accettano o restituiscono riferimenti. L'articolo Rust traits e generics tratta pattern generici avanzati.

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Annotazioni di Lifetime nella pratica: punti chiave

  • I lifetimes descrivono la validità dei riferimenti, non l'esistenza dei valori. Il borrow checker li utilizza per prevenire i dangling pointer a tempo di compilazione.
  • Le regole di elision gestiscono automaticamente la maggior parte dei casi. Le annotazioni esplicite diventano necessarie quando si restituiscono riferimenti derivati da più input.
  • I lifetimes delle struct esprimono la relazione outlives: qualsiasi struct contenente riferimenti deve essere parametrizzata dai lifetimes di quei riferimenti.
  • Il bound 'static sui generics significa "non contiene riferimenti non-statici", non "deve essere un riferimento".
  • Le struct autoreferenziali richiedono una gestione speciale attraverso Pin, codice unsafe o crate helper.
  • Le domande da colloquio si concentrano sul comprendere perché il codice non compila e come le annotazioni di lifetime cambiano le garanzie di validità.
Sfida del giorno

Sapresti trovare il bug in Rust?

Uno snippet reale, un bug nascosto, un tentativo al giorno. Senza account per provare.

Anthony Fillion-Maillet

Scritto da

Anthony Fillion-Maillet

Fondatore di SharpSkill

Sviluppatore fullstack da oltre 10 anni. Guida SharpSkill e risponde di tutto ciò che vi viene pubblicato.

Aggiornato il 27 agosto 2026

Tag

#rust
#lifetimes
#borrow-checker
#memory-safety
#interview

Condividi

Articoli correlati