Scope in Rust
Lo scope (ambito) è la zona di codice in cui una variabile è valida. È un’idea semplice, ma capirla bene fa andare al loro posto molte altre regole di Rust (soprattutto l’ownership).
Lo scope è definito dalle parentesi graffe
Sezione intitolata “Lo scope è definito dalle parentesi graffe”Una variabile esiste dal punto in cui viene dichiarata fino alla } che chiude il blocco in cui si trova:
fn main() { let x = 5; // qui x entra nello scope
{ let y = 10; // qui y entra nello scope println!("x = {x}, y = {y}"); // sono valide entrambe } // qui y esce dallo scope
println!("x = {x}"); // println!("{y}"); // ❌ errore: y non è più nello scope}Output:
x = 5, y = 10x = 5y esiste solo dentro il blocco { } interno. Quando quel blocco finisce, y sparisce: provare a usarla dopo è un errore di compilazione, non una sorpresa durante l’esecuzione.
Lo scope va in una sola direzione: un blocco interno vede le variabili dei blocchi che lo circondano (è così che il blocco interno stampa x), ma un blocco esterno non può guardare dentro uno interno.
Anche il corpo di una funzione è uno scope
Sezione intitolata “Anche il corpo di una funzione è uno scope”I parametri e le variabili che crei dentro una funzione esistono solo per la durata di quella chiamata:
fn calculate() -> i32 { let result = 5 * 2; // result esiste solo dentro calculate result}
fn main() { let value = calculate(); println!("{value}"); // println!("{result}"); // ❌ errore: qui fuori result non esiste}Output:
10Vale anche al contrario: una funzione non vede le variabili locali di main. Se le serve un valore, glielo passi come parametro, come hai visto nella pagina sulle Funzioni.
Anche i blocchi sono espressioni
Sezione intitolata “Anche i blocchi sono espressioni”Un blocco { } non serve solo a limitare dove vivono le variabili. Può anche produrre un valore: l’ultima espressione al suo interno, scritta senza punto e virgola. È la stessa regola che fa restituire a una funzione la sua ultima riga.
È comodo quando ti servono un paio di variabili di appoggio per calcolare qualcosa, ma non vuoi che restino in giro dopo:
fn main() { let price = 40;
let total = { let tax = price / 10; // tax vive solo dentro questo blocco let shipping = 5; price + tax + shipping // niente punto e virgola: è il valore del blocco };
println!("totale = {total}"); // println!("{tax}"); // ❌ errore: tax è fuori dallo scope}Output:
totale = 49Due dettagli a cui fare attenzione:
- L’ultima riga
price + tax + shippingnon ha il punto e virgola. Se lo aggiungi, il blocco restituisce()(niente) invece di un numero, eprintln!non riesce a stamparlo. - Il tutto è un’istruzione
let, quindi finisce con};dopo la graffa di chiusura.
Quando il blocco finisce, tax e shipping spariscono. Resta solo il risultato, total. Tenere le variabili di appoggio in un piccolo blocco come questo rende chiaro che servono per un solo calcolo.
Perché lo scope conta per l’ownership
Sezione intitolata “Perché lo scope conta per l’ownership”Questo si collega direttamente all’ownership (possesso), che incontrerai un po’ più avanti: un valore viene ripulito in automatico nel momento in cui finisce lo scope del suo proprietario.
fn main() { { let text = String::from("ciao"); println!("{text}"); } // qui finisce lo scope di text: Rust lo libera in automatico}Output:
ciaoÈ tutto qui il meccanismo per cui Rust non ha bisogno di un garbage collector: sa già, in fase di compilazione, esattamente dove finisce ogni scope.
Shadowing e scope
Sezione intitolata “Shadowing e scope”Ti ricordi lo shadowing (oscuramento) della pagina sulle Variabili? Ogni let con lo stesso nome dentro uno scope crea una variabile distinta: non cancella quella esterna, prende solo il suo posto per il resto di quello scope:
fn main() { let x = 5; { let x = x * 2; // una nuova x, solo dentro questo blocco println!("x interna = {x}"); // 10 } println!("x esterna = {x}"); // 5, non è cambiata}Output:
x interna = 10x esterna = 5Pratica
Sezione intitolata “Pratica”1. Correggi l’errore di scope
Sezione intitolata “1. Correggi l’errore di scope”Questo programma non compila, perché area viene usata dopo la fine del suo blocco:
fn main() { let width = 3; { let height = 4; let area = width * height; } println!("area = {area}");}Modificalo in modo che stampi area = 12, lasciando height solo dentro il blocco. Suggerimento: fai in modo che il blocco produca l’area, e salva il risultato in una variabile dichiarata fuori dal blocco.
Mostra la soluzione
fn main() { let width = 3; let area = { let height = 4; width * height }; println!("area = {area}");}Output:
area = 12Ora area vive nello scope di main, quindi println! la vede. height serve ancora solo dentro il blocco, e viene liberata quando il blocco finisce.
2. Livelli di annidamento
Sezione intitolata “2. Livelli di annidamento”Crea una variabile level uguale a 1. Apri un blocco che la oscura con 2 e, al suo interno, un altro blocco che la oscura con 3. Stampa level in ogni blocco, e di nuovo subito dopo la chiusura di ogni blocco. Suggerimento: dovresti vedere i numeri salire 1, 2, 3 e poi riscendere a 2 e 1.
Mostra la soluzione
fn main() { let level = 1; println!("livello {level}"); { let level = 2; println!("livello {level}"); { let level = 3; println!("livello {level}"); } println!("di nuovo al livello {level}"); } println!("di nuovo al livello {level}");}Output:
livello 1livello 2livello 3di nuovo al livello 2di nuovo al livello 1Ogni level interna nasconde quella esterna solo fino alla fine del suo blocco. Le variabili esterne non sono mai state modificate, quindi sono ancora lì ad aspettare.
Riepilogo
Sezione intitolata “Riepilogo”- Lo scope è la zona (delimitata da
{ }) in cui una variabile è valida. - Una variabile smette di esistere nell’istante in cui si arriva alla
}che chiude il suo scope. - Anche il corpo di una funzione è uno scope: le variabili locali spariscono quando la funzione termina.
- Un blocco è un’espressione: la sua ultima riga (senza punto e virgola) è il suo valore, e le variabili di appoggio spariscono quando finisce.
- È lo scope a dire a Rust esattamente quando liberare un valore, ed è per questo che non serve un garbage collector.
Verifica veloce
Sezione intitolata “Verifica veloce”Verifica veloce
1. Secondo questa pagina, perché Rust non ha bisogno di un garbage collector?
2. Che cosa succede se oscuri una variabile con let x = x * 2; dentro un blocco interno?
3. Quanto vale total dopo let total = { let a = 2; a * 10 };?
Punteggio: 0 / 3
Per approfondire
Sezione intitolata “Per approfondire”- Variable Scope — The Rust Book (in inglese): come funziona lo scope, spiegato come primo passo verso l’ownership.
- Scope and Shadowing — Rust by Example (in inglese): un breve esempio eseguibile di blocchi e shadowing.
- RAII — Rust by Example (in inglese): che cosa succede ai valori quando finisce il loro scope, per quando sei pronto per il passo successivo.
- Block expressions — The Rust Reference (in inglese): le regole esatte sui blocchi e sui valori che producono.