Introduzione
In questo articolo analizzeremo come il borrow checker di Rust determina la durata effettiva di un prestito mutabile (&mut) in presenza delle Non-Lexical Lifetimes (NLL).
In particolare, analizzeremo perché la durata di un prestito non coincide con la sua durata lessicale e perché chiamate esplicite a drop() non permettono di forzare manualmente la fine di un prestito quando esistono usi successivi nel flusso di controllo.
Problema tecnico
Durante lo studio del capitolo "4.1 - What is Ownership" del Rust Book assieme allo svolgimento dell'esercizio 06_move_semantics/move_semantics4.rs dei Rustlings, un programma apparentemente semplice che cerca di creare due prestiti mutabili consecutivi su x fallisce con un errore preciso, il E0499 che, se chiediamo al compilatore, ci darà questa spiegazione:
A variable was borrowed as mutable more than once.
Erroneous code example:
let mut i = 0;
let mut x = &mut i;
let mut a = &mut i;
x;
// error: cannot borrow `i` as mutable more than once at a time
Please note that in Rust, you can either have many immutable references, or one mutable reference.
Il codice incriminato è il seguente:
#[cfg(test)]
mod tests {
// TODO: Fix the compiler errors only by reordering the lines in the test.
// Don't add, change or remove any line.
#[test]
fn move_semantics4() {
let mut x = Vec::new();
let y = &mut x;
let z = &mut x;
y.push(42);
z.push(13);
assert_eq!(x, [42, 13]);
}
}
Come possiamo vedere, viene tentato un secondo &mut x mentre il primo è ancora (secondariamente) necessario.
Le regole della proprietà in Rust impongono di avere una sola referenza mutabile (&mut) attiva su un valore alla volta.
La correzione dell'esercizio è quindi molto semplice e diretta:
#[cfg(test)]
mod tests {
#[test]
fn move_semantics4() {
let mut x = Vec::new();
let y = &mut x;
y.push(42);
let z = &mut x;
z.push(13);
assert_eq!(x, [42, 13]);
}
}
Vediamo assieme perché funziona:
-
let y = &mut x;- Il compilatore registra: esiste un prestito mutabile attivo dixchiamatoy. -
y.push(42);- "Muovo" il valore, questo è l’ultimo uso diy. - Dopo questa riga, per il compilatore:
- non esistono più prestiti mutabili attivi su
x - quindi
let z = &mut x;diventa valido
- non esistono più prestiti mutabili attivi su
Non ero a conoscenza però di una feature particolare del compilatore di Rust, che consente proprio quanto abbiamo appena visto: le NLL: Non-Lexical Lifetimes.
Tramite le non-lexical lifetimes (NLL) il prestito mutabile termina al termine dell’ultimo uso, non necessariamente alla fine del blocco; questo sarà molto importante per l'esempio che vedremo a breve
Per mezzo delle Non-Lexical Lifetimes (NLL) il compilatore:
- determina che
ynon verrà più usato dopoy.push(42). - conclude che il lifetime del prestito mutabile di
ytermina esattamente qui.
Domanda: Esiste un modo per dichiarare esplicitamente un lifetime a y affinché il compilatore non utilizzi le NLL ma sia io tramite il codice a dirgli fin quando dura y?
Risposta: non si può dichiarare un lifetime esplicito su una variabile locale per forzare il borrow checker a ignorare le NLL.
I parametri del lifetime servono per firmare tipi/funzioni/strutture, non per controllare direttamente la durata lessicale di un binding dentro una funzione.
Pensando mi viene in mente che per controllare esplicitamente quando un prestito termina, conosco la funzione drop().
Allora penso.. Quindi se lo droppo, allora se provo ad invocarlo dopo let z = &mut x; non dovrebbe più darmi l'errore E0499 ma un altro, giusto?
Sbagliato.
fn main() {
let mut x = Vec::new();
let y = &mut x;
y.push(42);
drop(y);
let z = &mut x; // <-- E0499 qui: il prestito di y è ancora vivo per via dell'uso successivo
y.push(43); // <-- E0382 qui: uso di y dopo che è stato mosso da drop(y)
z.push(13);
}
Abbiamo due errori distinti:
-
E0499:cannot borrow 'x' as mutable more than once at a time— il secondo&mut xè dichiarato mentre il primo prestito è ancora considerato vivo. -
E0382:use of moved value 'y'—yè stato mosso dadrop(y)e viene poi usato.
Il motivo del comportamento non è un "bug" del compilatore ma la conseguenza della Non-Lexical Lifetimes (NLL): il borrow checker lavora su MIR (Mid-level Intermediate Representation) e usa analisi di liveness per estendere il lifetime di un prestito fino al suo ultimo uso identificato sul flusso di controllo.
Io avevo pensato che utilizzando drop(), y poi non sarebbe più stata disponibile, quindi invocandola successivamente a let z = &mut x mi avrebbe dato un errore diverso, non l'errore E0499.
In realtà non è così:
drop(y)termina un prestito solo se il binding non viene usato dopo.
Se c’è un uso successivo, mediante NLL il compilatore estende retroattivamente fino a quell’uso, rendendodrop(y)irrilevante ai fini del borrow.
Perché drop() non accorcia un borrow mutabile
Il comportamento che genera E0499 e E0382 è coerente con l'approccio moderno del compilatore Rust: NLL basato su MIR effettua liveness analysis e determina le regioni di borrow in base agli usi reali. drop(y) non è una "bacchetta magica" che termina automaticamente la regione di un prestito se esistono usi successivi; anzi, drop è un move che rende il binding non più utilizzabile.
Principio pratico: assicurati che l'ultimo uso del primo &mut preceda la creazione del successivo &mut. Le tecniche più robuste per farlo sono ambiti espliciti, estrazione in funzioni e riprogettazione dei dati per evitare aliasing mutabile non necessario.
E tu? Hai mai incontrato un caso simile?
Fammelo sapere nei commenti!
A presto
Mirko
Top comments (0)