Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Validare i Reference con la Lifetime

Le lifetime (longevità) sono un’altra tipologia di generico che abbiamo già utilizzato. Invece di garantire che un type abbia il comportamento desiderato, le lifetime assicurano che i reference siano validi finché ne abbiamo bisogno.

Un dettaglio che non abbiamo discusso nella sezione Reference e Borrowing del Capitolo 4 è che ogni reference in Rust ha una certa longevità, lifetime, che è lo scope per il quale quel reference è valido. Il più delle volte, la lifetime è implicita e inferita, proprio come il più delle volte i type sono inferiti. Siamo tenuti ad annotare il type solo quando sono possibili più type. Allo stesso modo, dobbiamo annotare la longevità quando la lifetime dei reference potrebbe essere correlata in diversi modi. Rust ci richiede di annotare le relazioni utilizzando parametri di lifetime generici per garantire che i reference utilizzati in fase di esecuzione siano e rimangano sicuramente validi.

Annotare la lifetime non è un concetto presente nella maggior parte degli altri linguaggi di programmazione, quindi questo ti sembrerà poco familiare. Sebbene non tratteremo la lifetime nella sua interezza in questo capitolo, discuteremo i modi più comuni in cui potresti incontrare la sintassi di lifetime in modo che tu possa familiarizzare con il concetto.

Reference Pendenti

Lo scopo principale della lifetime è prevenire i riferimenti pendenti (dangling references), che, se fossero presenti, causerebbere al programma in esecuzione di avere reference che fanno riferimento a dati a cui non dovrebbero far riferimento. Considera il programma nel Listato 10-16, che ha uno scope esterno e uno interno.

fn main() {
    let r;

    {
        let x = 5;
        r = &x;
    }

    println!("r: {r}");
}
Listato 10-16: Tentativo di utilizzare un reference il cui valore è uscito dallo scope

Nota: gli esempi nei Listati 10-16, 10-17 e 10-23 dichiarano variabili senza assegnare loro un valore iniziale, quindi il nome della variabile esiste nello scope esterno. A prima vista, questo potrebbe sembrare in conflitto con il fatto che Rust non abbia valori null. Tuttavia, se proviamo a utilizzare una variabile prima di assegnarle un valore, otterremo un errore in fase di compilazione, il che dimostra che Rust in effetti non ammette valori null.

Lo scope esterno dichiara una variabile denominata r senza valore iniziale, mentre lo scope interno dichiara una variabile denominata x con valore iniziale 5. Nello scope interno, proviamo a impostare il valore di r come reference a x. Quindi lo scope interno termina e proviamo a stampare il valore in r. Questo codice non verrà compilato perché il valore a cui fa riferimento r è uscito dallo scope prima che proviamo ad utilizzarlo. Ecco il messaggio di errore:

$ cargo run
   Compiling capitolo10 v0.1.0 (file:///progetti/capitolo10)
error[E0597]: `x` does not live long enough
 --> src/main.rs:6:13
  |
5 |         let x = 5;
  |             - binding `x` declared here
6 |         r = &x;
  |             ^^ borrowed value does not live long enough
7 |     }
  |     - `x` dropped here while still borrowed
8 |
9 |     println!("r: {r}");
  |                  --- borrow later used here

For more information about this error, try `rustc --explain E0597`.
error: could not compile `capitolo10` (bin "capitolo10") due to 1 previous error

Il messaggio di errore indica che la variabile x “non vive abbastanza a lungo”. Il motivo è che x sarà fuori dallo scope quando lo scope interno termina alla riga 7. Ma r è ancora valido per lo scope esterno; Poiché il suo scope è più ampio, diciamo che “vive più a lungo”. Se Rust permettesse a questo codice di funzionare, r farebbe riferimento alla memoria de-allocata quando x è uscita dallo scope, e qualsiasi cosa provassimo a fare con r non funzionerebbe correttamente. Quindi, come fa Rust a determinare che questo codice non è valido? Utilizza un borrow checker.

Il Borrow Checker

Il compilatore Rust ha un borrow checker (controllore dei prestiti) che confronta gli scope per determinare se tutti i dati presi in prestito tramite reference sono validi. Il Listato 10-17 mostra lo stesso codice del Listato 10-16 ma con annotazioni che mostrano la longevità delle variabili.

fn main() {
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {r}");   //          |
}                         // ---------+
Listato 10-17: Annotazioni delle lifetime di r e x, denominate rispettivamente 'a e 'b

Qui abbiamo annotato la lifetime di r con 'a e la lifetime di x con 'b. Come puoi vedere, il blocco 'b interno è molto più piccolo del blocco 'a esterno. In fase di compilazione, Rust confronta la dimensione delle due longevità e vede che r ha una lifetime 'a ma che si riferisce alla memoria con una lifetime 'b. Il programma viene rifiutato perché 'b è più breve di 'a: il soggetto del reference non ha la stessa longevità del reference stesso.

Il Listato 10-18 corregge il codice in modo che non abbia un reference pendente e si compili senza errori.

fn main() {
    let x = 5;            // ----------+-- 'b
                          //           |
    let r = &x;           // --+-- 'a  |
                          //   |       |
    println!("r: {r}");   //   |       |
                          //   |       |
}                         // --+-------+
Listato 10-18: Un reference valido perché i dati hanno una longevità maggiore del reference

Qui, x ha la longevità 'b, che in questo caso è maggiore di 'a. Questo significa che r può fare riferimento a x perché Rust sa che il reference in r sarà sempre valido finché x è valido.

Ora che sai cosa sono le lifetime dei reference e come Rust analizza la longevità per garantire che i reference siano sempre validi, esploriamo le lifetime generiche dei parametri e dei valori di ritorno nel contesto delle funzioni.

Lifetime Generica nelle Funzioni

Scriveremo una funzione che restituisce la più lunga tra due slice di stringa. Questa funzione prenderà due slice e ne restituirà una singola. Dopo aver implementato la funzione più_lunga, il codice nel Listato 10-19 dovrebbe stampare La stringa più lunga è abcd.

File: src/main.rs
fn main() {
    let stringa1 = String::from("abcd");
    let stringa2 = "xyz";

    let risultato = più_lunga(stringa1.as_str(), stringa2);
    println!("La stringa più lunga è {}", risultato);
}
Listato 10-19: Una funzione main che chiama la funzione più_lunga per trovare la più lunga tra due slice

Nota che vogliamo che la funzione accetti slice, che sono reference, piuttosto che stringhe, perché non vogliamo che la funzione più_lunga prenda possesso dei suoi parametri. Fai riferimento a Slice di Stringa come Parametri” nel Capitolo 4 per una disamina più approfondita sul motivo per cui i parametri che utilizziamo nel Listato 10-19 sono quelli che desideriamo.

Se proviamo a implementare la funzione più_lunga come mostrato nel Listato 10-20, non verrà compilata.

File: src/main.rs
fn main() {
    let stringa1 = String::from("abcd");
    let stringa2 = "xyz";

    let risultato = più_lunga(stringa1.as_str(), stringa2);
    println!("La stringa più lunga è {}", risultato);
}

fn più_lunga(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}
Listato 10-20: Un’implementazione della funzione più_lunga che restituisce la più lunga tra due stringhe ma non viene ancora compilata

Invece, otteniamo il seguente errore che parla di lifetime:

$ cargo run
   Compiling capitolo10 v0.1.0 (file:///projects/capitolo10)
error[E0106]: missing lifetime specifier
 --> src/main.rs:9:35
  |
9 | fn più_lunga(x: &str, y: &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 `x` or `y`
help: consider introducing a named lifetime parameter
  |
9 | fn più_lunga<'a>(x: &'a str, y: &'a str) -> &'a str {
  |             ++++     ++          ++          ++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `capitolo10` (bin "capitolo10") due to 1 previous error

Il testo di aiuto rivela che il type restituito necessita di un parametro di lifetime generico perché Rust non riesce a stabilire se il reference restituito si riferisce a x o y. In realtà, non lo sappiamo anche perché il blocco if nel corpo di questa funzione restituisce un reference a x e il blocco else restituisce un reference a y!

Quando definiamo questa funzione, non conosciamo i valori concreti che verranno passati a questa funzione, quindi non sappiamo se verrà eseguito il caso if o il caso else. Non conosciamo nemmeno la longevità concreta dei riferimenti che verranno passati, quindi non possiamo esaminare gli scope come abbiamo fatto nei Listati 10-17 e 10-18 per determinare se il reference restituito sarà sempre valido. Nemmeno il borrow checker può determinarlo, perché non sa come la longevità di x e y si relaziona alla longevità del valore di ritorno. Per correggere questo errore, aggiungeremo parametri di lifetime generici che definiscono la relazione tra i reference in modo che il borrow checker possa eseguire la sua analisi.

Sintassi dell’Annotazione di Lifetime

Le annotazioni di lifetime non modificano la longevità di alcun reference. Piuttosto, descrivono e dettagliano le relazioni tra le longevità di più riferimenti senza andare a modificarla. Proprio come le funzioni possono accettare qualsiasi type quando la firma specifica un parametro di type generico, le funzioni possono accettare reference con qualsiasi longevità specificando un parametro di lifetime generico.

Le annotazioni di lifetime hanno una sintassi leggermente insolita: i nomi dei parametri di lifetime devono iniziare con un apostrofo (') e sono solitamente tutti in minuscolo e molto brevi, come i type generici. La maggior parte delle persone usa il nome 'a per la prima annotazione di lifetime. Posizioniamo le annotazioni dei parametri di lifetime dopo la & di un reference, utilizzando uno spazio per separare l’annotazione dal type del reference.

Ecco alcuni esempi:

&i32        // `reference` senza parametro di longevità
&'a i32     // `reference` con annotazione della longevità
&'a mut i32 // `reference` mutabile con annotazione della longevità

Un’annotazione di longevità di per sé non ha molto significato perché le annotazioni servono a indicare a Rust come i parametri di lifetime generici di più reference si relazionano tra loro. Esaminiamo come le annotazioni di longevità si relazionano tra loro nel contesto della funzione più_lunga.

Nella Firma delle Funzioni

Per utilizzare le annotazioni di longevità nelle firme delle funzioni, dobbiamo dichiarare i parametri lifetime generici all’interno di parentesi angolari tra il nome della funzione e l’elenco dei parametri, proprio come abbiamo fatto con i parametri type generici.

Vogliamo che la firma esprima la seguente restrizione: il reference restituito sarà valido finché entrambi i parametri saranno validi. Questa è la relazione tra le lifetime dei parametri e il valore restituito. Chiameremo la lifetime 'a e la aggiungeremo a ciascun reference, come mostrato nel Listato 10-21.

File: src/main.rs
fn main() {
    let stringa1 = String::from("abcd");
    let stringa2 = "xyz";

    let risultato = più_lunga(stringa1.as_str(), stringa2);
    println!("La stringa più lunga è {}", risultato);
}

fn più_lunga<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
Listato 10-21: La definizione di funzione più_lunga specifica che tutti i reference nella firma devono avere la stessa lifetime 'a

Questo codice dovrebbe compilarsi e produrre il risultato desiderato quando lo utilizziamo con la funzione main del Listato 10-19.

La firma della funzione ora indica a Rust che per un certo lifetime 'a, la funzione accetta due parametri, entrambi slice di stringa che durano almeno quanto la lifetime 'a. La firma della funzione indica anche a Rust che la slice di stringa restituita dalla funzione avrà una longevità massima pari al lifetime 'a. In pratica, significa che la longevità del reference restituito dalla funzione più_lunga è minore o uguale alla minore tra le longevità dei valori a cui fanno riferimento gli argomenti della funzione. Queste relazioni sono ciò che vogliamo che Rust utilizzi quando analizza questo codice.

Ricorda, quando specifichiamo i parametri di longevità nella firma di questa funzione, non stiamo modificando le longevità dei valori passati o restituiti. Piuttosto, stiamo specificando che il borrow checker deve rifiutare qualsiasi valore che non rispetta questi vincoli. Nota che la funzione più_lunga non ha bisogno di sapere esattamente quanto dureranno x e y, ma solo che esiste uno scope che può essere sostituito ad 'a che soddisfi questa firma.

Quando si annotano le longevità nelle funzioni, le annotazioni vanno nella firma della funzione, non nel corpo della funzione. Le annotazioni di lifetime diventano parte del contratto della funzione, proprio come i type nella firma. Avere le firme delle funzioni che contengono il contratto di longevità significa che l’analisi effettuata dal compilatore Rust può essere più semplice. Se c’è un problema con il modo in cui una funzione è annotata o con il modo in cui viene chiamata, gli errori del compilatore possono indicare la parte del nostro codice e le restrizioni in modo più preciso. Se, invece, il compilatore Rust facesse più inferenze su ciò che intendevamo che fossero le relazioni tra le longevità, il compilatore potrebbe essere in grado di indicare solo un utilizzo del nostro codice molto lontano dalla causa del problema.

Quando passiamo reference concreti a più_lunga, la longevità concreta che viene sostituita per 'a è la parte dello scope di x che si sovrappone allo scope di y. In altre parole, la longevità generica 'a otterrà la longevità concreta uguale alla minore tra le longevità di x e y. Poiché abbiamo annotato il reference restituito con lo stesso parametro di longevità 'a, il reference restituito sarà valido anche per la lunghezza della minore tra la longevità di x e y.

Osserviamo come le annotazioni di longevità limitino la funzione più_lunga dal ricevere reference con longevità concrete diverse. Il Listato 10-22 è un esempio semplice.

File: src/main.rs
fn main() {
    let stringa1 = String::from("una stringa bella lunga");

    {
        let stringa2 = String::from("xyz");
        let risultato = più_lunga(stringa1.as_str(), stringa2.as_str());
        println!("La stringa più lunga è {risultato}");
    }
}

fn più_lunga<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
Listato 10-22: Utilizzo della funzione più_lunga con reference a valori String con longevità concrete diverse

In questo esempio, stringa1 è valida fino alla fine dello scope esterno, stringa2 è valida fino alla fine dello scope interno e risultato fa riferimento a qualcosa che è valido fino alla fine dello scope interno. Esegui questo codice e vedrai che verrà approvato dal borrow checker; compilerà e stamperà La stringa più lunga è una stringa bella lunga.

Proviamo ora un esempio che mostra come la lifetime del reference in risultato deve essere la lifetime più breve tra i due argomenti. Sposteremo la dichiarazione della variabile risultato al di fuori dello scope interno, ma lasceremo l’assegnazione del valore alla variabile risultato all’interno dello scope con stringa2. Quindi sposteremo println! che utilizza risultato al di fuori dello scope interno, dopo che quest’ultimo è terminato. Il codice nel Listato 10-23 non verrà compilato.

File: src/main.rs
fn main() {
    let stringa1 = String::from("una stringa bella lunga");
    let risultato;
    {
        let stringa2 = String::from("xyz");
        risultato = più_lunga(stringa1.as_str(), stringa2.as_str());
    }
    println!("La stringa più lunga è {risultato}");
}

fn più_lunga<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
Listato 10-23: Tentativo di utilizzare risultato dopo che stringa2 è uscita dallo scope

Quando proviamo a compilare questo codice, otteniamo questo errore:

$ cargo run
   Compiling capitolo10 v0.1.0 (file:///projects/capitolo10)
error[E0597]: `stringa2` does not live long enough
 --> src/main.rs:6:50
  |
5 |         let stringa2 = String::from("xyz");
  |             -------- binding `stringa2` declared here
6 |         risultato = più_lunga(stringa1.as_str(), stringa2.as_str());
  |                                                  ^^^^^^^^ borrowed value does not live long enough
7 |     }
  |     - `stringa2` dropped here while still borrowed
8 |     println!("La stringa più lunga è {risultato}");
  |                                       --------- borrow later used here

For more information about this error, try `rustc --explain E0597`.
error: could not compile `capitolo10` (bin "capitolo10") due to 1 previous error

L’errore indica che, affinché risultato sia valido per l’istruzione println!, stringa2 dovrebbe essere valido fino alla fine dello scope esterno. Rust lo sa perché abbiamo annotato le lifetime dei parametri della funzione e del valore di ritorno utilizzando lo stesso parametro 'a.

Come esseri umani, possiamo guardare questo codice e vedere che stringa1 è più lungo di stringa2 e, pertanto, risultato conterrà un reference a stringa1. Poiché stringa1 non è ancora uscito dallo scope, un reference a stringa1 sarà ancora valido per l’istruzione println!. Tuttavia, il compilatore non può verificare che il reference sia valido in questo caso. Abbiamo detto a Rust che la lifetime del reference restituito dalla funzione più_lunga è uguale alla più breve tra le lifetime dei riferimenti passati. Pertanto, il borrow checker non consente il codice nel Listato 10-23 in quanto potrebbe contenere un reference non valido.

Prova a progettare altri esperimenti che varino i valori e le lifetime dei reference passati alla funzione più_lunga e il modo in cui il reference restituito viene utilizzato. Formula ipotesi sul fatto che questi esperimenti supereranno o meno il borrow checker prima di compilare; poi verifica se avevi ragione!

Relazioni

Il modo in cui è necessario specificare i parametri di longevità dipende da cosa sta facendo la tua funzione. Ad esempio, se modificassimo l’implementazione della funzione più_lunga in modo che restituisca sempre il primo parametro anziché la slice di stringa più lunga, non avremmo bisogno di specificare una lifetime per il parametro y. Il codice seguente verrà compilato:

File: src/main.rs
fn main() {
    let stringa1 = String::from("abcd");
    let stringa2 = "efghijklmnopqrstuvwxyz";

    let risultato = più_lunga(stringa1.as_str(), stringa2);
    println!("La stringa più lunga è {risultato}");
}

fn più_lunga<'a>(x: &'a str, y: &str) -> &'a str {
    x
}

Abbiamo specificato un parametro di longevità 'a per il parametro x e il type di ritorno, ma non per il parametro y, perché la lifetime di y non ha alcuna relazione con la lifetime di x o con il valore di ritorno.

Quando si restituisce un reference da una funzione, il parametro di longevità per il type di ritorno deve corrispondere al parametro di longevità per uno dei parametri. Se il reference restituito non fa riferimento ad uno dei parametri, deve fare riferimento ad un valore creato all’interno di questa funzione. Tuttavia, questo sarebbe un reference pendente perché il valore uscirà dallo scope al termine della funzione. Considera questo tentativo di implementazione della funzione più_lunga che non verrà compilato:

File: src/main.rs
fn main() {
    let stringa1 = String::from("abcd");
    let stringa2 = "xyz";

    let risultato = più_lunga(stringa1.as_str(), stringa2);
    println!("La stringa più lunga è {risultato}");
}

fn più_lunga<'a>(x: &str, y: &str) -> &'a str {
    let risultato = String::from("una stringa bella lunga");
    risultato.as_str()
}

Qui, anche se abbiamo specificato un parametro di longevità 'a per il type di ritorno, questa implementazione non verrà compilata perché la lifetime del valore di ritorno non è affatto correlata alla lifetime dei parametri. Ecco il messaggio di errore che riceviamo:

$ cargo run
   Compiling capitolo10 v0.1.0 (file:///progetti/capitolo10)
error[E0515]: cannot return value referencing local variable `risultato`
  --> src/main.rs:11:5
   |
11 |     risultato.as_str()
   |     ---------^^^^^^^^^
   |     |
   |     returns a value referencing data owned by the current function
   |     `risultato` is borrowed here

For more information about this error, try `rustc --explain E0515`.
error: could not compile `capitolo10` (bin "capitolo10") due to 1 previous error; 2 warnings emitted

Il problema è che risultato esce dallo scope e viene de-allocato alla fine della funzione più_lunga. Stiamo anche cercando di restituire un reference a risultato dalla funzione. Non c’è modo di specificare parametri di longevità che modifichino il reference pendente, e Rust non ci permette di creare un reference pendente. In questo caso, la soluzione migliore sarebbe restituire un type piuttosto che un reference, in modo che la funzione chiamante sia responsabile della de-allocazione del valore.

In definitiva, la sintassi di longevità serve a collegare le lifetime dei vari parametri e valori di ritorno delle funzioni. Una volta messi in relazione, Rust ha informazioni sufficienti per consentire operazioni che proteggono la memoria e impedire operazioni che creerebbero reference pendenti o comunque violerebbero la sicurezza della memoria.

Nella Definizione delle Struct

Finora, tutte le struct che abbiamo definito contenevano type con ownership. Possiamo definire struct che contengano reference, ma in tal caso dovremmo aggiungere un’annotazione di longevità su ogni reference nella definizione della struct. Il Listato 10-24 ha una struct denominata ParteImportante che contiene una slice di stringa.

File: src/main.rs
struct ParteImportante<'a> {
    parte: &'a str,
}

fn main() {
    let romanzo = String::from("Chiamami Ishmael. Alcuni anni fa...");
    let prima_frase = romanzo.split('.').next().unwrap();
    let i = ParteImportante {
        parte: prima_frase,
    };
}
Listato 10-24: Una struct che contiene un reference, che richiede un’annotazione di longevità

Questa struct ha il singolo campo parte che contiene una slice di stringa, che è un reference. Come per i type generici, dichiariamo il nome del parametro lifetime generico tra parentesi angolari dopo il nome della struct, in modo da poter utilizzare il parametro lifetime nel corpo della definizione della struct. Questa annotazione significa che un’istanza di ParteImportante avrà una longevità non superiore a quella del reference che contiene nel suo campo parte.

La funzione main qui crea un’istanza della struct ParteImportante che contiene un reference alla prima frase della String di proprietà della variabile romanzo. I dati in romanzo esistono prima che l’istanza di ParteImportante venga creata. Inoltre, romanzo non esce dallo scope finché anche ParteImportante non esce dallo scope, quindi il reference nell’istanza di ParteImportante è valido.

Elidere la Lifetime

Hai imparato che ogni reference ha una lifetime e che è necessario specificare parametri di longevità per funzioni o struct che utilizzano reference. Tuttavia, avevamo una funzione nel Listato 4-9, mostrata di nuovo nel Listato 10-25, che veniva compilata senza annotazioni di longevità.

File: src/lib.rs
fn prima_parola(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &lettera) in bytes.iter().enumerate() {
        if lettera == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {
    let mia_stringa = String::from("hello world");

    // `prima_parola` funziona con slice di `String`
    let parola = prima_parola(&mia_stringa[..]);

    let mia_stringa_letterale = "hello world";

    // `prima_parola` funziona con slice di letterali di stringa
    let parola_letterale = prima_parola(&mia_stringa_letterale[..]);

    // E siccome i letterali di stringa *sono* già delle slice,
    // funziona pure così, senza usare la sintassi delle slice!
    let parola = prima_parola(mia_stringa_letterale);
}
Listato 10-25: Una funzione che abbiamo definito nel Listato 4-9 che è stata compilata senza annotazioni di longevità, anche se il parametro e il type restituito sono reference

Il motivo per cui questa funzione viene compilata senza annotazioni di longevità è storico: nelle prime versioni (precedenti alla 1.0) di Rust, questo codice non sarebbe stato compilato perché ogni reference necessitava di una lifetime esplicita. A quel tempo, la firma della funzione sarebbe stata scritta in questo modo:

fn prima_parola<'a>(s: &'a str) -> &'a str {

Dopo aver scritto molto codice Rust, il team Rust ha scoperto che i programmatori Rust inserivano le stesse annotazioni di longevità più e più volte in particolari situazioni. Queste situazioni erano prevedibili e seguivano alcuni schemi deterministici. Gli sviluppatori hanno programmato questi schemi nel codice del compilatore in modo che il borrow checker potesse dedurre le lifetime in queste situazioni e non avesse bisogno di annotazioni esplicite.

Questo pezzo di storia di Rust è rilevante perché è possibile che emergano e vengano aggiunti al compilatore altri schemi deterministici. In futuro, potrebbero essere necessarie ancora meno annotazioni di longevità.

Gli schemi programmati nell’analisi dei riferimenti di Rust sono chiamati regole di elisione della longevità (lifetime elision rules). Queste non sono regole che i programmatori devono seguire; sono un insieme di casi particolari che il compilatore prenderà in considerazione e, se il codice si adatta a questi casi, non sarà necessario esplicitare le lifetime.

Le regole di elisione non forniscono un’inferenza completa. Se persiste un’ambiguità sulle lifetime dei reference dopo che Rust ha applicato le regole, il compilatore non inferirà quale dovrebbe essere la longevità dei reference rimanenti. E quindi, invece di indovinare, il compilatore genererà un errore indicando dove è necessario aggiungere le annotazioni di longevità.

Le longevità dei parametri di funzione o metodo sono chiamate lifetime di input, e le longevità dei valori di ritorno sono chiamate lifetime di output.

Il compilatore utilizza tre regole per calcolare le lifetime dei reference quando non ci sono annotazioni esplicite. La prima regola si applica ai lifetime di input, mentre la seconda e la terza regola si applicano ai lifetime di output. Se il compilatore arriva alla fine delle tre regole e ci sono ancora reference per i quali non riesce a calcolare la longevità, il compilatore si interromperà con un errore. Queste regole si applicano sia alle definizioni fn che ai blocchi impl.

La prima regola è che il compilatore assegna un parametro di lifetime a ogni parametro che è un reference. In altre parole, una funzione con un parametro riceve un parametro di lifetime: fn foo<'a>(x: &'a i32); una funzione con due parametri riceve due parametri di lifetime separati: fn foo<'a, 'b>(x: &'a i32, y: &'b i32); e così via.

La seconda regola è che, se c’è esattamente un parametro di lifetime in input, quel lifetime viene assegnato a tutti i parametri di lifetime in output: fn foo<'a>(x: &'a i32) -> &'a i32.

La terza regola è che, se ci sono più parametri di lifetime in input, ma uno di questi è &self o &mut self perché si tratta di un metodo, la lifetime di self viene assegnata a tutti i parametri di lifetime in output. Questa terza regola rende i metodi molto più facili da leggere e scrivere perché sono necessari meno simboli.

Facciamo finta di essere il compilatore. Applicheremo queste regole per calcolare le longevità dei reference nella firma della funzione prima_parola nel Listato 10-25. La firma inizia senza alcuna lifetime associata ai reference:

fn prima_parola(s: &str) -> &str {

Quindi il compilatore applica la prima regola, che specifica che ogni parametro abbia una propria longevità. La chiameremo 'a come al solito, quindi ora la firma è questa:

fn prima_parola<'a>(s: &'a str) -> &str {

La seconda regola si applica perché esiste esattamente un singolo parametro di longevità in input. La seconda regola specifica che la longevità di un parametro in input viene assegnata alla longevità in output, quindi la firma è ora questa:

fn prima_parola<'a>(s: &'a str) -> &'a str {

Ora tutti i reference in questa firma di funzione hanno una longevità e il compilatore può continuare la sua analisi senza che il programmatore debba annotare le lifetime in questa firma di funzione.

Diamo un’occhiata a un altro esempio, questa volta utilizzando la funzione più_lunga che non aveva parametri di longevità quando abbiamo iniziato a lavorarci nel Listato 10-20:

fn più_lunga(x: &str, y: &str) -> &str {

Applichiamo la prima regola: ogni parametro ha la propria longevità. Questa volta abbiamo due parametri invece di uno, quindi abbiamo due lifetime:

fn più_lunga<'a, 'b>(x: &'a str, y: &'b str) -> &str {

Puoi già notare che la seconda regola non si applica perché c’è più di una lifetime di input. Nemmeno la terza regola si applica, perché più_lunga è una funzione e non un metodo, quindi nessuno dei parametri è self. Dopo aver elaborato tutte e tre le regole, non abbiamo ancora capito qual è la longevità del type di ritorno. Ecco perché abbiamo ricevuto un errore durante la compilazione del codice nel Listato 10-20: il compilatore ha elaborato le regole di elisione della lifetime, ma non è comunque riuscito a calcolare tutte le lifetime dei reference nella firma.

Poiché la terza regola si applica solo alle firme dei metodi, esamineremo le lifetime in quel contesto per capire perché la terza regola ci consente di non dover annotare la longevità nelle firme dei metodi nella maggior parte dei casi.

Nella Definizione dei Metodi

Quando implementiamo metodi su una struct con lifetime, utilizziamo la stessa sintassi dei parametri di type generico, come mostrato nel Listato 10-11. Il punto in cui dichiariamo e utilizziamo i parametri di longevità dipende dal fatto che siano correlati ai campi della struct o ai parametri del metodo e ai valori di ritorno.

I nomi delle lifetime per i campi della struct devono sempre essere dichiarati dopo la parola chiave impl e quindi utilizzati dopo il nome della struct, poiché tali lifetime fanno parte del type della struct.

Nelle firme dei metodi all’interno del blocco impl, i reference potrebbero essere legati alla longevità dei reference nei campi della struct, oppure potrebbero essere indipendenti. Inoltre, le regole di elisione della lifetime spesso fanno sì che le annotazioni della longevità non siano necessarie nelle firme dei metodi. Diamo un’occhiata ad alcuni esempi utilizzando la struct denominata ParteImportante che abbiamo definito nel Listato 10-24.

Per prima cosa useremo un metodo chiamato livello il cui unico parametro è un reference a self e il cui valore di ritorno è un i32, che non è un reference ad alcunché:

struct ParteImportante<'a> {
    parte: &'a str,
}

impl<'a> ParteImportante<'a> {
    fn livello(&self) -> i32 {
        3
    }
}

impl<'a> ParteImportante<'a> {
    fn annunciare_e_restituire_parte(&self, annuncio: &str) -> &str {
        println!("Attenzione per favore: {annuncio}");
        self.parte
    }
}

fn main() {
    let romanzo = String::from("Chiamami Ishmael. Qualche anno fa...");
    let prima_frase = romanzo.split('.').next().unwrap();
    let i = ParteImportante {
        parte: prima_frase,
    };
}

La dichiarazione del parametro lifetime dopo impl e il suo utilizzo dopo il nome del type sono obbligatori ma, grazie alla prima regola di elisione, non siamo tenuti ad annotare la longevità del reference a self.

Ecco un esempio in cui si applica la terza regola di elisione della lifetime:

struct ParteImportante<'a> {
    parte: &'a str,
}

impl<'a> ParteImportante<'a> {
    fn livello(&self) -> i32 {
        3
    }
}

impl<'a> ParteImportante<'a> {
    fn annunciare_e_restituire_parte(&self, annuncio: &str) -> &str {
        println!("Attenzione per favore: {annuncio}");
        self.parte
    }
}

fn main() {
    let romanzo = String::from("Chiamami Ishmael. Qualche anno fa...");
    let prima_frase = romanzo.split('.').next().unwrap();
    let i = ParteImportante {
        parte: prima_frase,
    };
}

Ci sono due lifetime in input, quindi Rust applica la prima regola di elisione della lifetime e assegna sia a &self che a annuncio le corrispettive lifetime. Quindi, poiché uno dei parametri è &self, al type di ritorno viene assegnata la lifetime di &self. Ora tutte le lifetime sono state considerate.

La Lifetime Statica

Una lifetime speciale di cui dobbiamo discutere è 'static, che indica che la longevità del reference interessato corrisponde a quella del programma. Tutti i letterali stringa hanno la lifetime 'static, che possiamo annotare come segue:

#![allow(unused)]
fn main() {
let s: &'static str = "Ho una lifetime statica.";
}

Il testo di questa stringa è memorizzato direttamente nel binario del programma, che è sempre disponibile. Pertanto, la longevità di tutti i letterali stringa è 'static.

Potresti trovare suggerimenti nei messaggi di errore del compilatore di utilizzare la lifetime 'static. Ma prima di specificare 'static come lifetime per un reference, valuta se quel reference ha effettivamente necessità di rimanere valido per l’intera durata dell’esecuzione del tuo programma. Nella maggior parte dei casi, un messaggio di errore che suggerisce la lifetime 'static deriva dal tentativo di creare un reference pendente o da una mancata corrispondenza delle longevità disponibili. In questi casi, la soluzione è risolvere questi problemi, non specificare la lifetime 'static.

Parametri di Type Generico, Vincoli del Trait e Lifetime

Esaminiamo brevemente la sintassi per specificare parametri di type generico, vincoli di trait e lifetime, tutto in un’unica funzione!

fn main() {
    let stringa1 = String::from("abcd");
    let stringa2 = "xyz";

    let risultato = più_lunga_con_annuncio(
        stringa1.as_str(),
        stringa2,
        "Oggi è il compleanno di qualcuno!",
    );
    println!("La stringa più lunga è {risultato}");
}

use std::fmt::Display;

fn più_lunga_con_annuncio<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: Display,
{
    println!("Annuncio! {ann}");
    if x.len() > y.len() { x } else { y }
}

Questa è la funzione più_lunga del Listato 10-21 che restituisce la più lunga tra due slice. Ma ora ha un parametro aggiuntivo denominato ann di type generico T, che può ricevere qualsiasi type che implementi il trait Display come specificato dalla clausola where. Questo parametro aggiuntivo verrà stampato utilizzando {}, motivo per cui il vincolo del trait Display è necessario. Poiché le lifetime sono un type generico, le dichiarazioni del parametro di longevità 'a e del parametro di type generico T vanno nella stessa lista all’interno delle parentesi angolari dopo il nome della funzione.

Abbiamo trattato molti argomenti in questo capitolo! Ora che conosci i parametri di type generico, i trait, i vincoli dei trait e i parametri di lifetime generici, sei pronto a scrivere codice senza ripetizioni che funzioni in molte situazioni diverse. I parametri di type generico consentono di applicare il codice a type diversi. I trait e i vincoli dei trait garantiscono che, anche se i type sono generici, abbiano il comportamento di cui il codice ha bisogno. Hai imparato come usare le annotazioni di longevità per garantire che questo codice flessibile non abbia reference pendenti. E tutta questa analisi avviene in fase di compilazione, il che non influisce sulle prestazioni in fase di esecuzione!

Che ci crediate o no, c’è molto altro da imparare sugli argomenti trattati in questo capitolo: il Capitolo 18 tratta degli oggetti trait, che rappresentano un altro modo di utilizzare i trait. Esistono anche scenari più complessi che coinvolgono le annotazioni della lifetime, che ti serviranno solo in scenari molto avanzati; se ti può interessare dovresti leggere The Rust Reference (in inglese). Ma ora imparerai come scrivere test in Rust in modo da poterti assicurare che il tuo codice funzioni a dovere.