Tracciare le reference: i lifetime

Implicitamente, ogni reference ha una cosiddetta lifetime, ovvero uno scope per cui quella reference rimane valida. La maggior parte delle volte, le lifetime sono gestite implicitamente dal compilatore, così come la maggior parte delle volte i tipi possono essere inferiti dal compilatore. Come vedremo tra poco però, a volte dobbiamo annotare esplicitamente quando potrebbero avere relazioni diverse con altre lifetime, così come dobbiamo annotare i tipi se devono essere generici all'interno del codice.

L'annotazione di lifetime è un concetto che la maggior parte degli altri linguaggi di programmazione non ha, quindi potrebbe inizialmente essere un argomento abbastanza ostico. Per fortuna, i casi in cui bisogna annotare le lifetime sono rari nei programmi comuni.

A cosa servono le lifetime?

Lo scopo principale delle lifetime è quello di prevenire le reference invalide, ovvero reference che puntano a oggetti che non esistono più o che non sono più validi. Ad esempio, prendiamo il seguente codice

fn main() {
    let reference;

    {
        let value = 5;
        reference = &value;
    }

    println!("The value is {}", reference);
}

Questo codice non compila, anche se potrebbe sembrare a prima vista corretto: la reference è dichiarata nella funzione main, quindi è presente nell'ambiente quando viene privata a stampare. Il problema è che il valore value viene eliminato prima, quindi la reference diventa invalida. L'errore che ci restituisce il compilatore è il seguente:

error[E0597]: `value` does not live long enough
 --> src/main.rs:6:21
  |
5 |         let value = 5;
  |             ----- binding `value` declared here
6 |         reference = &value;
  |                     ^^^^^^ borrowed value does not live long enough
7 |
8 |     }
  |     - `value` dropped here while still borrowed
9 |     println!("The value is {}", reference);
  |                                 --------- borrow later used here

In effetti, ci viene detto che la variabile value non "vive abbastanza a lungo", infatti la value vive solo nel blocco interno, mentre reference vive per tutta la funzione main. Il borrow checker controlla che la vita (lifetime) dei valori sia più "grande" o uguale alla vita delle corrispettive reference, per assicurarsi che tutte le reference siano valide.

Ad esempio il programma cambiato in questo modo

fn main() {
    let value = 5;
    let reference = &value;
    println!("The value is {}", reference);
}

Compila correttamente, in quanto la lifetime di value è almeno grande quanto quella di reference.

fn main() {
    let value = 5;                       //---------------------+ lifetime
    let reference = &value;              // -+  lifetime di     | di value
    println!("ref {}", reference);       //  |  reference       |
                                         // -+                  |
                                         // --------------------+
}

Lifetime generiche nelle funzioni

Proviamo adesso a scrivere una funzione che prende in ingresso due stringhe e ritorna la più lunga. Vogliamo prendere in ingresso due reference a str e ritornare una reference a str, perché non vogliamo che la nostra funzione prenda ownership delle stringhe in ingresso.

Proviamo quindi a scrivere la funzione

#![allow(unused)]
fn main() {
fn longest(s1: &str, s2: &str) -> &str {
  if s1.len() > s2.len() {
    s1
  } else {
    s2
  }
}
}

ma otteniamo il seguente errore.

error[E0106]: missing lifetime specifier
 --> src/main.rs:1:35
  |
1 | fn longest(s1: &str, s2: &str) -> &str {
  |                ----      ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value,
    but the signature does not say whether it is borrowed from `s1` or `s2`
help: consider introducing a named lifetime parameter
  |
1 | fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
  |           ++++      ++           ++          ++

error: lifetime may not live long enough
 --> src/main.rs:3:9
  |
1 | fn longest(s1: &str, s2: &str) -> &str {
  |                - let's call the lifetime of this reference `'1`
2 |     if s1.len() > s2.len() {
3 |         s1
  |         ^^ returning this value requires that `'1` must outlive `'static`

error: lifetime may not live long enough
 --> src/main.rs:5:9
  |
1 | fn longest(s1: &str, s2: &str) -> &str {
  |                          - let's call the lifetime of this reference `'2`
...
5 |         s2
  |         ^^ returning this value requires that `'2` must outlive `'static`

For more information about this error, try `rustc --explain E0106`.

Quello che questo errore ci sta dicendo è che il compilatore non riesce a inferire il lifetime della reference di ritorno, in quanto potrebbe essere uguale al lifetime di s1 oppure a quello di s2. Di fatto, non possiamo neanche noi stabilirlo a priori, dipende dalle stringhe passate alla funzione a tempo di esecuzione.

Possiamo però annotare le reference con un lifetime, con la sintassi che segue

#![allow(unused)]
fn main() {
&str        // reference a una str
&'a str     // reference a una str con la lifetime 'a
&'a mut str // reference mutabile a una str con la lifetime 'a
}

Possiamo quindi rendere generica la funzione longest rispetto alla lifetime in questo modo

#![allow(unused)]
fn main() {
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
}

In particolare tramite <'a> stiamo dicendo che la funzione è parametrica rispetto a un lifetime 'a, e che s1, s2 e il valore di ritorno devono essere "contenute" all'interno della stessa lifetime. Un altro modo di vederla è che la lifetime della reference ritornata è uguale alla più piccola lifetime tra quella di s1 e quella di s2. Questo intuitivamente ha senso: siccome non possiamo sapere a priori quale delle due reference verrà ritornata, la lifetime è quella più piccola.

Ad esempio, possiamo usare la funzione longest in questo modo

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let s1 = String::from("abcd");
    let s2 = String::from("xyz");

    let result = longest(s1.as_str(), s2.as_str());
    println!("La stringa più lunga è '{}'", result);
}

Fino a qua niente di strano, entrambe s1 e s2 hanno lifetime uguale al blocco della funzione main. Vediamo però che succede quando proviamo a usare la funzione longest in un altro modo

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let s1 = String::from("abcd");
    let result;
    {
      let s2 = String::from("xyz");
      result = longest(s1.as_str(), s2.as_str());
    }
    println!("La stringa più lunga è '{}'", result);
}

Otteniamo il seguente errore di compilazione

error[E0597]: `s2` does not live long enough
  --> src/main.rs:13:37
   |
12 |       let s2 = String::from("xyz");
   |           -- binding `s2` declared here
13 |       result = longest(s1.as_str(), s2.as_str());
   |                                     ^^ borrowed value does not live long enough
14 |     }
   |     - `s2` dropped here while still borrowed
15 |     println!("La stringa più lunga è '{}'", result);
   |                                             ------ borrow later used here

For more information about this error, try `rustc --explain E0597`.

Questo errore ci dice che la seconda reference non vive abbastanza a lungo, nonostante poi sappiamo che il risultato sarà una reference valida (sarà una reference a s1). Il compilatore Rust, però, non può fare assunzioni sul valore delle stringhe, e quindi impedisce la compilazione.

Esiste una lifetime speciale, ovvero 'static, che rappresenta una reference che è valida per tutta l'esecuzione del programma. Esempi possono essere reference a stringhe costanti oppure a variabili globali.

Le annotazioni di lifetime esplicite possono anche essere inserite nella definizione di strutture, per esempio per stabilire una relazione tra le lifetime dei membri della struttura. Per ulteriori dettagli è possibile consultare la documentazione ufficiale di Rust.