Salta ai contenuti

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).

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 = 10
x = 5
▶ Provalo tu

y 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.

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:

10
▶ Provalo tu

Vale 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.

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 = 49
▶ Provalo tu

Due dettagli a cui fare attenzione:

  • L’ultima riga price + tax + shipping non ha il punto e virgola. Se lo aggiungi, il blocco restituisce () (niente) invece di un numero, e println! 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.

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
▶ Provalo tu

È 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.

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 = 10
x esterna = 5
▶ Provalo tu

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 = 12
▶ Provalo tu

Ora area vive nello scope di main, quindi println! la vede. height serve ancora solo dentro il blocco, e viene liberata quando il blocco finisce.

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 1
livello 2
livello 3
di nuovo al livello 2
di nuovo al livello 1
▶ Provalo tu

Ogni 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.

  • 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

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