Shopping cart

0

Cart

  • 0 item

Nessun prodotto nel carrello.

All categories
 Corso Gratuito di Programmazione Rust Lezione 036 – Convalida dei riferimenti con i tempi di vita

SE VUOI PRENDERE LA CERTIFICAZIONE PER QUESTO CORSO CLICCA QUI

Validazione di Riferimenti con Lifetimes

I lifetimes sono un’altra forma di generici che abbiamo già utilizzato. Piuttosto che garantire che un tipo abbia il comportamento desiderato, i lifetimes assicurano che i riferimenti siano validi per tutto il tempo in cui ne abbiamo bisogno.

Un dettaglio che non abbiamo discusso nella sezione “Riferimenti e Prestiti” nel Capitolo 4 è che ogni riferimento in Rust ha un lifetime, che è lo scope per il quale quel riferimento è valido. La maggior parte delle volte, i lifetimes sono impliciti e dedotti, proprio come la maggior parte delle volte, i tipi sono dedotti. Dobbiamo annotare i tipi solo quando sono possibili più tipi. In modo simile, dobbiamo annotare i lifetimes quando i lifetimes dei riferimenti potrebbero essere correlati in modi diversi. Rust ci richiede di annotare le relazioni usando parametri generici di lifetime per garantire che i riferimenti effettivi utilizzati a runtime siano sicuramente validi.

Annotare i lifetimes non è un concetto che la maggior parte degli altri linguaggi di programmazione ha, quindi questo sarà qualcosa di nuovo. Anche se non tratteremo i lifetimes nella loro interezza in questo capitolo, discuteremo dei modi comuni in cui potresti incontrare la sintassi dei lifetimes in modo che tu possa familiarizzare con il concetto.

Prevenzione di Riferimenti Pendenti con Lifetimes

Il principale obiettivo dei lifetimes è prevenire riferimenti pendenti, che causano a un programma di fare riferimento a dati diversi dai dati che si intende fare riferimento. Considera il programma nell’Esempio 10-16, che ha uno scope esterno e uno scope interno.

rust

fn main() {
let r;
{
let x = 5;
r = &x;
}

println!("r: {}", r);
}

Lo scope esterno dichiara una variabile chiamata r senza valore iniziale, e lo scope interno dichiara una variabile chiamata x con il valore iniziale di 5. All’interno dello scope interno, cerchiamo di impostare il valore di r come un riferimento a x. Poi lo scope interno termina, e cerchiamo di stampare il valore in r. Questo codice non verrà compilato perché il valore a cui si riferisce r è uscito dallo scope prima che proviamo a utilizzarlo.

Il motivo è che x sarà fuori dallo scope quando lo scope interno termina alla riga 7. Ma r è ancora valido per lo scope esterno; perché il suo scope è più grande, diciamo che “vive più a lungo”. Se Rust permettesse a questo codice di funzionare, r farebbe riferimento alla memoria deallocata quando x è uscito dallo scope, e qualsiasi cosa cerchiamo di fare con r non funzionerebbe correttamente. Allora come fa Rust a determinare che questo codice è invalido? Utilizza un controllo dei prestiti.

Il Controllo dei Prestiti

Il compilatore Rust ha un controllo dei prestiti che confronta gli scope per determinare se tutti i prestiti sono validi. L’Esempio 10-17 mostra lo stesso codice dell’Esempio 10-16 ma con annotazioni che mostrano i lifetimes delle variabili.

rust

fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
} // ---------+

Qui, abbiamo annotato il lifetime di r con ‘a e il lifetime di x con ‘b. Come puoi vedere, il blocco interno ‘b è molto più piccolo del blocco esterno ‘a. A tempo di compilazione, Rust confronta le dimensioni dei due lifetimes e vede che r ha un lifetime di ‘a ma che si riferisce a una memoria con un lifetime di ‘b. Il programma viene rifiutato perché ‘b è più breve di ‘a: l’oggetto del riferimento non vive per tutto il tempo del riferimento.

L’Esempio 10-18 corregge il codice in modo che non abbia un riferimento pendente e compili senza errori.

rust

fn main() {
let x = 5; // ----------+-- 'b
// |
let r = &x; // --+-- 'a |
// | |
println!("r: {}", r); // | |
// --+ |
} // ----------+

Qui, x ha il lifetime ‘b, che in questo caso è più grande di ‘a. Questo significa che r può fare riferimento a x perché Rust sa che il riferimento in r sarà sempre valido mentre x è valido.

Ora che sai dove sono i lifetimes dei riferimenti e come Rust analizza i lifetimes per garantire che i riferimenti siano sempre validi, esploriamo i lifetimes generici dei parametri e dei valori di ritorno nel contesto delle funzioni.

Lifetimes Generici nelle Funzioni

Scriveremo una funzione che restituisce la stringa più lunga tra due sottostringhe. Questa funzione prenderà due sottostringhe e restituirà una singola sottostringa. Dopo aver implementato la funzione longest, il codice nell’Esempio 10-19 dovrebbe stampare “La stringa più lunga è abcd”.

rust

fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";

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

Nota che vogliamo che la funzione prenda delle sottostringhe, che sono riferimenti, piuttosto che delle stringhe, perché non vogliamo che la funzione longest prenda il possesso dei suoi parametri. Consulta la sezione “Sottostringhe come Parametri” nel Capitolo 4 per ulteriori discussioni su perché i parametri che usiamo nell’Esempio 10-19 sono quelli che vogliamo.

Se proviamo a implementare la funzione longest come mostrato nell’Esempio 10-20, non verrà compilata.

rust

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

Invece, otteniamo il seguente errore che parla di lifetimes:

rust

error[E0106]: missing lifetime specifier
--> src/main.rs:9:33
|
9 | fn longest(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 longest<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `chapter10` due to previous error

Il testo di aiuto rivela che il tipo di ritorno ha bisogno di un parametro generico di lifetime perché Rust non può dire se il riferimento restituito si riferisce a x o a y. In realtà, non lo sappiamo neanche noi, perché il blocco if nel corpo di questa funzione restituisce un riferimento a x e il blocco else restituisce un riferimento a y!

Quando stiamo definendo questa funzione, non conosciamo i valori concreti che verranno passati in questa funzione, quindi non sappiamo se il caso if o il caso else verrà eseguito. Non conosciamo neanche i lifetimes concreti dei riferimenti che verranno passati, quindi non possiamo guardare agli scope come abbiamo fatto negli Esempi 10-17 e 10-18 per determinare se il riferimento che restituiamo sarà sempre valido. Nemmeno il controllo dei prestiti può determinarlo, perché non sa come i lifetimes di x e y si relazionano al lifetime del valore di ritorno. Per correggere questo errore, aggiungeremo dei parametri generici di lifetime che definiscono la relazione tra i riferimenti in modo che il controllo dei prestiti possa eseguire la sua analisi.

Sintassi delle Annotazioni di Lifetimes

Le annotazioni di lifetimes non modificano quanto tempo vivono i riferimenti. Piuttosto, descrivono le relazioni dei lifetimes di più riferimenti tra loro senza influenzare i lifetimes. Proprio come le funzioni possono accettare qualsiasi tipo quando la firma specifica un parametro di tipo generico, le funzioni possono accettare riferimenti con qualsiasi lifetime specificando un parametro di lifetime generico.

Le annotazioni di lifetimes hanno una sintassi leggermente insolita: i nomi dei parametri di lifetime devono iniziare con un apostrofo (‘) e sono di solito tutti in minuscolo e molto brevi, come i tipi generici. La maggior parte delle persone usa il nome ‘a per la prima annotazione di lifetime. Mettiamo le annotazioni dei parametri di lifetime dopo il & di un riferimento, usando uno spazio per separare l’annotazione dal tipo del riferimento.

Ecco alcuni esempi: un riferimento a un i32 senza un parametro di lifetime, un riferimento a un i32 che ha un parametro di lifetime chiamato ‘a, e un riferimento mutabile a un i32 che ha anche il lifetime ‘a.

rust

&i32 // un riferimento
&'a i32 // un riferimento con un lifetime esplicito
&'a mut i32 // un riferimento mutabile con un lifetime esplicito

Una singola annotazione di lifetime di per sé non ha molto significato, perché le annotazioni sono destinate a dire a Rust come i parametri di lifetime generici di più riferimenti sono correlati tra loro. Esaminiamo come le annotazioni di lifetime si relazionano tra loro nel contesto della funzione longest.

Annotazioni di Lifetimes nelle Firme delle Funzioni

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

Vogliamo che la firma esprima il seguente vincolo: il riferimento restituito sarà valido fintanto che entrambi i parametri sono validi. Questa è la relazione tra i lifetimes dei parametri e il valore di ritorno. Daremo il nome al lifetime ‘a e poi lo aggiungeremo a ciascun riferimento, come mostrato nell’Esempio 10-21.

rust

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

Questo codice dovrebbe compilare e produrre il risultato desiderato quando lo usiamo con la funzione principale nell’Esempio 10-19.

La firma della funzione dice a Rust che per qualche lifetime ‘a, la funzione prende due parametri, entrambi dei quali sono sottostringhe che vivono almeno quanto il lifetime ‘a. La firma della funzione dice anche a Rust che la sottostringa restituita dalla funzione vivrà almeno quanto il lifetime ‘a. In pratica, significa che il lifetime del riferimento restituito dalla funzione longest è lo stesso del minore dei lifetimes dei valori a cui si riferiscono gli argomenti della funzione. Queste relazioni sono ciò che vogliamo che Rust utilizzi quando analizza questo codice.

Ricorda, quando specifichiamo i parametri di lifetime in questa firma della funzione, non stiamo cambiando i lifetimes di nessun valore passato o restituito. Piuttosto, stiamo specificando che il controllo dei prestiti dovrebbe rifiutare qualsiasi valore che non rispetti questi vincoli. Nota che la funzione longest non ha bisogno di sapere esattamente quanto tempo vivranno x e y, solo che qualche scope può essere sostituito per ‘a che soddisferà questa firma.

Quando si annotano i lifetimes 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 tipi nella firma. Avere le firme delle funzioni contenere il contratto di lifetime significa che l’analisi che fa il 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 i vincoli in modo più preciso. Se, invece, il compilatore Rust facesse più inferenze su quali fossero le relazioni dei lifetimes che intendevamo, il compilatore potrebbe solo indicare un uso del nostro codice a molte fasi di distanza dalla causa del problema.

Quando passiamo riferimenti concreti a longest, il lifetime concreto che viene sostituito per ‘a è la parte dello scope di x che si sovrappone allo scope di y. In altre parole, il lifetime generico ‘a otterrà il lifetime concreto che è uguale al minore dei lifetimes di x e y. Poiché abbiamo annotato il riferimento restituito con lo stesso parametro di lifetime ‘a, il riferimento restituito sarà anche valido per la durata del minore dei lifetimes di x e y.

Diamo uno sguardo a come le annotazioni di lifetime limitano la funzione longest passando riferimenti con lifetimes concreti diversi. L’Esempio 10-22 è un esempio diretto.

rust

fn main() {
let string1 = String::from("una stringa lunga è lunga");

{
let string2 = String::from("xyz");
let result = longest(string1.as_str(), string2.as_str());
println!("La stringa più lunga è {}", result);
}
}

In questo esempio, string1 è valida fino alla fine dello scope esterno, string2 è valida fino alla fine dello scope interno e result fa riferimento a qualcosa che è valido fino alla fine dello scope interno. Esegui questo codice e vedrai che il controllo dei prestiti approva; verrà compilato e stamperà “La stringa più lunga è una stringa lunga è lunga”.

Ora, proviamo un esempio che mostra che il lifetime del riferimento in result deve essere il lifetime più piccolo dei due argomenti. Sposteremo la dichiarazione della variabile result al di fuori dello scope interno ma lasceremo l’assegnazione del valore alla variabile result all’interno dello scope con string2. Poi sposteremo il println! che usa result all’esterno dello scope interno, dopo che lo scope interno è terminato. Il codice nell’Esempio 10-23 non compilerà.

rust

fn main() {
let string1 = String::from("una stringa lunga è lunga");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
}
println!("La stringa più lunga è {}", result);
}

Quando cerchiamo di compilare questo codice, otteniamo questo errore:

rust

$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `string2` does not live long enough
--> src/main.rs:6:44
|
6 | result = longest(string1.as_str(), string2.as_str());
| ^^^^^^^^^^^^^^^^ borrowed value does not live long enough
7 | }
| - `string2` dropped here while still borrowed
8 | println!("La stringa più lunga è {}", result);
| ------ borrow later used here

For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` due to previous error

L’errore mostra che per rendere valido result per l’istruzione println!, string2 dovrebbe essere valido fino alla fine dello scope esterno. Rust lo sa perché abbiamo annotato i lifetimes dei parametri di funzione e dei valori di ritorno usando lo stesso parametro di lifetime ‘a.

Come esseri umani, possiamo guardare questo codice e vedere che string1 è più lunga di string2 e quindi result conterrà un riferimento a string1. Poiché string1 non è ancora uscita dallo scope, un riferimento a string1 sarà ancora valido per l’istruzione println!. Tuttavia, il compilatore non può vedere che il riferimento è valido in questo caso. Abbiamo detto a Rust che il lifetime del riferimento restituito dalla funzione longest è lo stesso del minore dei lifetimes dei riferimenti passati. Pertanto, il controllo dei prestiti impedisce il codice nell’Esempio 10-23 poiché potrebbe avere un riferimento non valido.

Prova a progettare più esperimenti che variano i valori e i lifetimes dei riferimenti passati nella funzione longest e come viene utilizzato il riferimento restituito. Fai ipotesi su se i tuoi esperimenti supereranno o meno il controllo dei prestiti prima di compilare; quindi controlla se hai ragione!

Pensare in Termini di Lifetimes

Quando scrivi funzioni in Rust, è importante considerare i parametri di lifetime. Questi parametri specificano quanto tempo devono vivere i riferimenti che la tua funzione utilizza. Dipende da cosa fa la tua funzione per decidere come specificare i parametri di lifetime. Ad esempio, se cambiamo l’implementazione della funzione longest per restituire sempre il primo parametro anziché la sottostringa più lunga, non avremmo bisogno di specificare un lifetime per il parametro y. Ecco un esempio:

rust

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

Qui abbiamo specificato un parametro di lifetime ‘a per il parametro x e per il tipo di ritorno, ma non per il parametro y, perché il lifetime di y non ha alcuna relazione con il lifetime di x o con il valore di ritorno.

Quando restituisci un riferimento da una funzione, il parametro di lifetime per il tipo di ritorno deve corrispondere al parametro di lifetime per uno dei parametri. Se il riferimento restituito non si riferisce a uno dei parametri, deve riferirsi a un valore creato all’interno di questa funzione. Tuttavia, questo sarebbe un riferimento insensato perché il valore uscirà dallo scope alla fine della funzione. Considera questa implementazione tentata della funzione longest che non compilerà:

rust

fn longest<'a>(x: &str, y: &str) -> &'a str {
let result = String::from("una stringa davvero lunga");
result.as_str()
}

In questo caso, anche se abbiamo specificato un parametro di lifetime ‘a per il tipo di ritorno, questa implementazione non compilerà perché il lifetime del valore di ritorno non è correlato al lifetime dei parametri. Ecco il messaggio di errore che otteniamo:

sql

error[E0515]: cannot return reference to local variable `result`
--> src/main.rs:11:5
|
11 | result.as_str()
| ^^^^^^^^^^^^^^^ returns a reference to data owned by the current function

Il problema è che result esce dallo scope e viene pulito alla fine della funzione longest. Inoltre, stiamo cercando di restituire un riferimento a result dalla funzione. Non c’è modo di specificare parametri di lifetime che cambierebbero il riferimento insensato, e Rust non ci permetterà di creare un riferimento insensato.

In definitiva, la sintassi dei lifetimes serve a collegare i lifetimes di vari parametri e valori di ritorno delle funzioni. Una volta collegati, Rust ha informazioni sufficienti per consentire operazioni sicure in memoria e vietare operazioni che creerebbero puntatori insensati o violerebbero in altro modo la sicurezza della memoria.

Annotazioni di Lifetimes nelle Definizioni di Struct

Le strutture in Rust possono contenere tipi di dati posseduti o riferimenti. Quando definisci strutture che contengono riferimenti, devi aggiungere un’annotazione di lifetime su ogni riferimento nella definizione della struttura. Ecco un esempio:

rust

struct ImportantExcerpt<'a> {
part: &'a str,
}

Questa struttura ha un singolo campo, part, che contiene una sottostringa, ossia un riferimento. Come nei tipi di dati generici, dichiari il nome del parametro di lifetime generico tra parentesi angolari dopo il nome della struttura in modo da poter usare il parametro di lifetime nel corpo della definizione della struttura. Questa annotazione significa che un’istanza di ImportantExcerpt non può sopravvivere al riferimento che detiene nel campo part.

La funzione principale qui crea un’istanza della struttura ImportantExcerpt che detiene un riferimento alla prima frase della stringa posseduta dalla variabile novel. I dati in novel esistono prima che l’istanza di ImportantExcerpt sia creata. Inoltre, novel non esce dallo scope fino dopo che ImportantExcerpt è uscito dallo scope, quindi il riferimento nell’istanza di ImportantExcerpt è valido.

Elisione dei Lifetimes

In Rust, ci sono regole per determinare i lifetimes dei riferimenti quando non ci sono annotazioni esplicite. Queste regole sono chiamate elisione dei lifetimes. Sono regole che il compilatore segue per inferire i lifetimes dei riferimenti, e se il tuo codice corrisponde a queste regole, non è necessario scrivere esplicitamente i lifetimes.

Le tre regole principali sono:

  1. Ogni parametro di riferimento ha un lifetime.
  2. Se c’è un solo parametro di input lifetime, quel lifetime viene assegnato a tutti i parametri di output lifetime.
  3. Se ci sono più parametri di input lifetime, ma uno di essi è &self o &mut self perché si tratta di un metodo, il lifetime di self viene assegnato a tutti i parametri di output lifetime.

Queste regole semplificano la scrittura e la lettura del codice Rust. Ad esempio, considera la funzione first_word mostrata in Listing 10-25. Non è necessario specificare esplicitamente i lifetimes nei parametri e nel tipo di ritorno della funzione, perché il compilatore può inferirli seguendo queste regole.

In sostanza, le annotazioni di lifetime e le regole di elisione dei lifetimes sono strumenti potenti per scrivere codice Rust sicuro e leggibile. Con un’adeguata comprensione di queste tecniche, puoi affrontare molte situazioni diverse con fiducia e chiarezza.

1 Comment

Leave a Reply

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *