Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Rust: gestione della memoria

Rust e gestione della memoria per la sicurezza. Analizziamo le regole di ownership e borrowing e la loro importanza in compilazione
Rust e gestione della memoria per la sicurezza. Analizziamo le regole di ownership e borrowing e la loro importanza in compilazione
Link copiato negli appunti

Sicuro ed efficiente: così potrebbe essere definito il linguaggio Rust. Il motivo di ciò è il suo meccanismo di gestione dei riferimenti includendo l'ownership e il borrowing ed è per questo che quella che sta per iniziare è una delle lezioni più importanti dell'intera guida. Il centro di tutto è il modo in cui Rust gestisce la memoria tramite il meccanismo dell'ownership che consta essenzialmente di un insieme di regole: il codice verrà compilato solo se tali regole saranno state rispettate e ciò garantirà che l'eseguibile ottenuto sia effettivamente sicuro. Tali concetti sono al centro della cosiddetta memory safety di Rust.

Le regole dell'ownership in Rust

Le regole dell'ownership sono tre:

  1. ogni valore in Rust ha un proprietario, un owner per usare il termine originale;
  2. ogni valore può avere un solo owner in un dato istante;
  3. quando la variabile che rappresenta l'owner esce dallo scope, il valore viene rimosso dalla memoria.

Soprattutto il terzo punto è particolarmente caratteristico di Rust. Infatti, una variabile è in scope dal momento in cui è dichiarata al momento in cui il suo blocco di codice termina, in genere con la chiusura della relativa parentesi graffa. L'uscita dallo scope innesca l'automatica eliminazione dalla memoria della variabile senza l'uso di garbage collector come in Java né responsabilità di deallocazione in capo al programmatore come in C/C++.

Tutto il seguente discorso non riguarda tipi di dato semplici a dimensione fissa visti in una lezione precedente bensì tipi a lunghezza variabile come le stringhe. Il motivo di ciò è che i dati a dimensione "non fissa" sono allocati nel cosiddetto heap ovvero una porzione della memoria dove le sequenze di byte sono gestite dinamicamente e le informazioni lì depositate hanno modo di aumentare di volume in base alle necessità. Le variabili che usiamo per indicare tali allocazioni di memoria non contengono i dati direttamente ma solo il loro "indirizzo" al fine di porteli individuare.

Problemi di ownership

Visto il rigore con cui il compilatore verifica le condizioni sull'owner, spesso il rustacean novizio potrebbe trovarsi di fronte ad una serie di errori in grado di scoraggiare anche i più volenterosi. Proprio queste segnalazioni di errore sono il meccanismo che Rust usa per fare in modo che la memoria sia sempre usata in modo opportuno. Vediamo subito un tipico caso di errore:

let s=String::from("ciao");
let s1=s;
println!("s vale {} e s1 vale {}", s,s1);

In questo caso, creiamo una stringa e tentiamo di farne una copia in s1: potremmo dire, del codice del tutto innocuo, almeno apparentemente. Per la regola dell'ownership, la stringa era originariamente di s ma dopo let s1=s questa ne ha perso la proprietà. Al momento di eseguire il println quindi, l'owner della stringa è s1 e, per la seconda regola poco fa enunciata, s non è più valido non potendo esistere due owner contemporaneamente. Tale codice, per questi motivi, non può essere compilato.

Il problema di perdita della proprietà si intensificherebbe con le funzioni. Proviamo a immaginare cosa succederebbe se venisse creata una variabile in una funzione: a meno di restituirne il valore, la fine della funzione chiuderebbe lo scope distruggendo quanto da noi creato.

La soluzione: dall'ownership al borrowing

Se il problema risiede nella proprietà (ownership) la soluzione è il prestito (il borrowing). Potremmo "prestare" le informazioni memorizzate ad un'altra variabile per il tempo che ne ha bisogno per poi farcela restituire. Proviamo un esempio compilabile (pertanto, corretto e memory safe!) in cui creiamo una stringa nel main, la passiamo ad una funzione che la manda in output e, per concludere, la stampiamo nuovamente alla fine del main:

fn elabora_stringa(stringa: &String) {
 println!("Elaborazione nella funzione: {}", stringa);
}
fn main() {
  let s=String::from("Messaggio da elaborare");
  elabora_stringa(&s);
  println!("Stampa nel main: {}", s);
}

Se non fosse per le due & (&), il programma non sarebbe compilabile sebbene non elaboriamo la stringa ma ci limitiamo a stamparla. Per esercizio, si può provare ad eliminare questi due simboli e leggere gli errori che si ottengono.

Con gli & quella variabile diventa un reference ovvero una sorta di puntatore all'owner che a sua volta punta ai dati. Il vantaggio di ciò è che non passando l'owner all'interno della funzione, questo non uscirà mai dallo scope ed i suoi dati non saranno distrutti. Chi verrà passato sarà il riferimento che può esser distrutto senza compromettere le informazioni allocate.

Mutabilità in Rust

Quello che abbiamo visto è un esempio "in lettura", senza modifiche. Ora però dobbiamo imparare ad utilizzare reference per effettuare modifiche. Vediamo un esempio.

fn elabora_stringa(stringa: &mut String) {
  stringa.push_str("... Ciao!");
 println!("Dopo la modifica: {}", stringa);
}
fn main() {
  let mut s=String::from("Messaggio da elaborare");
  println!("Prima della modifica: {}", s);
  elabora_stringa(&mut s);
  println!("Stampa nel main: {}", s);
}

Nell'esempio, impostiamo una stringa nel main, la passiamo (tramite reference) ad una funzione in cui verrà modifica con una semplice concatenazione e la stampiamo di nuovo nel main dopo la modifica. Ecco l'output:

Prima della modifica: Messaggio da elaborare
Dopo la modifica: Messaggio da elaborare: Ciao!
Stampa nel main: Messaggio da elaborare: Ciao!

Notiamo che l'esito della modifica sarà persistente infatti all'uscita dalla funzione, all'interno del main, la stringa manterrà la concatenazione ricevuta.

Per poter modificare ciò che viene puntato da un reference, è necessario usare mut e qui l'abbiamo applicato tre volte: nella dichiarazione della stringa, nella definizione della funzione e al momento di invocarla.

Conclusioni

Al momento, possiamo fermarci qui. Ci sarebbero molti altri esempi da visionare per scoprire ulteriori problematiche e soluzioni ma, ora come ora, possiamo accontentarci. Portiamo con noi, l'importanza di comprendere cos'è un owner e cosa si intende per reference ma soprattutto ricordiamo che tutti gli errori in compilazione che riscontreremo saranno sempre frutto delle politiche che Rust applica per la sicurezza delle nostre applicazioni.

Ti consigliamo anche