Rust è famoso per il suo sistema di gestione della memoria basato su concetti come ownership, borrowing e lifetimes, che consentono di scrivere codice sicuro senza garbage collector. Questi concetti possono sembrare complessi all’inizio, ma sono essenziali per capire come Rust previene errori comuni come use-after-free o data races nei programmi concorrenti.
In questa lezione, esploreremo come funzionano ownership, borrowing e lifetimes, con esempi pratici per consolidare la comprensione.
In Rust, ogni valore ha un proprietario (owner), e ci sono regole precise che determinano come avviene il trasferimento di ownership:
fn main() {
let s1 = String::from("Ciao");
let s2 = s1; // Ownership trasferita a s2
// println!("{}", s1); // Errore: s1 non è più valido
println!("{}", s2);
}
In questo caso, quando assegniamo s1
a s2
, l’ownership di s1
viene trasferita a s2
. Pertanto, s1
non è più valido.
Il movimento dell’ownership evita duplicati non sicuri di dati. Tuttavia, se vuoi duplicare un valore, puoi usare il metodo clone
:
fn main() {
let s1 = String::from("Ciao");
let s2 = s1.clone(); // Clonazione esplicita
println!("{}", s1); // Ora s1 è ancora valido
println!("{}", s2);
}
Per evitare di trasferire l’ownership, Rust permette di prendere in prestito un valore, sia in modo immutabile che mutabile.
Un riferimento immutabile (&T
) consente di leggere i dati senza modificarli.
fn stampa(s: &String) {
println!("{}", s);
}
fn main() {
let s1 = String::from("Ciao");
stampa(&s1); // Prende in prestito s1
println!("{}", s1); // s1 è ancora valido
}
Puoi avere più riferimenti immutabili contemporaneamente:
fn main() {
let s = String::from("Ciao");
let r1 = &s;
let r2 = &s;
println!("{} e {}", r1, r2);
}
Un riferimento mutabile (&mut T
) permette di modificare i dati, ma ci sono restrizioni:
fn aggiungi(s: &mut String) {
s.push_str(", mondo!");
}
fn main() {
let mut s1 = String::from("Ciao");
aggiungi(&mut s1); // Prende in prestito in modo mutabile
println!("{}", s1);
}
I lifetimes sono un meccanismo che assicura che i riferimenti siano sempre validi. Rust utilizza le lifetimes per evitare errori di memoria come i riferimenti pendenti (dangling references).
fn crea_stringa() -> &String {
let s = String::from("Ciao");
&s // Errore: s esce dallo scope
}
In questo esempio, la stringa s
viene creata all’interno della funzione e viene liberata appena la funzione termina, lasciando un riferimento non valido.
Puoi utilizzare annotazioni di lifetime per specificare quanto a lungo un riferimento è valido. Un esempio comune è con i parametri di una funzione:
fn maggiore<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let str1 = String::from("Ciao");
let str2 = String::from("Rust");
let risultato = maggiore(&str1, &str2);
println!("La stringa più lunga è: {}", risultato);
}
Qui, l’annotazione 'a
indica che il riferimento restituito ha la stessa durata dei parametri.
I concetti di ownership, borrowing e lifetimes sono il fondamento della gestione sicura della memoria in Rust. Anche se inizialmente possono sembrare complessi, padroneggiarli ti aiuterà a scrivere codice robusto e privo di errori legati alla memoria. Nel prossimo articolo esporeremo concetti come Strutture, Enum e Pattern Matching. Continua a esercitarti con gli esempi forniti e vedrai che questi concetti diventeranno naturali nel tuo sviluppo con Rust!