Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Cos’è la Ownership?

La ownership è un insieme di regole che disciplinano la gestione della memoria da parte di un programma Rust. Tutti i programmi devono gestire il modo in cui utilizzano la memoria del computer durante l’esecuzione. Alcuni linguaggi hanno una garbage collection che cerca regolarmente la memoria non più utilizzata durante l’esecuzione del programma; in altri linguaggi, il programmatore deve allocare e rilasciare esplicitamente la memoria. Rust utilizza un terzo approccio: la memoria viene gestita attraverso un sistema di controllo esclusivo con un insieme di regole che il compilatore controlla. Se una qualsiasi delle regole viene violata, il programma non viene compilato. Nessuna delle caratteristiche di ownership rallenterà il tuo programma mentre è in esecuzione.

Poiché la ownership è un concetto nuovo per molti programmatori, ci vuole un po’ di tempo per abituarsi. La buona notizia è che più si acquisisce esperienza con Rust e con le regole del sistema di ownership, più sarà facile sviluppare naturalmente codice sicuro ed efficiente. Non mollare!

Quando capirai la ownership, avrai una solida base per comprendere le caratteristiche che rendono Rust unico. In questo capitolo imparerai la ownership lavorando su alcuni esempi che si concentrano su una struttura di dati molto comune: le stringhe.

Lo Stack e l’Heap

Molti linguaggi di programmazione non richiedono di pensare allo stack e all’heap molto spesso. Ma in un linguaggio di programmazione di sistema come Rust, il fatto che un valore sia sullo stack o nell’heap influisce sul comportamento del linguaggio e sul motivo per cui devi prendere determinate decisioni.

Sia lo stack che l’heap sono parti di memoria disponibili per il codice da utilizzare in fase di esecuzione, ma sono strutturate in modi diversi. Lo stack memorizza i valori nell’ordine in cui li ottiene e rimuove i valori nell’ordine opposto. Questo viene definito last in, first out (LIFO). Pensa a una pila di piatti: quando aggiungi altri piatti, li metti in cima alla pila e quando ti serve un piatto, ne togli uno dalla cima. Aggiungere o rimuovere i piatti dal centro o dal fondo non funzionerebbe altrettanto bene! L’aggiunta di dati sullo stack viene definita push (immissione), mentre la rimozione dei dati viene definita pop (estrazione). Tutti i dati archiviati sullo stack devono avere una dimensione nota e fissa. I dati con una dimensione sconosciuta in fase di compilazione o una dimensione che potrebbe cambiare devono invece essere archiviati nell’heap.

L’heap è meno organizzato: quando metti i dati nell’heap, richiedi una certa quantità di spazio. L’allocatore di memoria trova un punto vuoto nell’heap che sia sufficientemente grande, lo contrassegna come in uso e restituisce un puntatore, che è l’indirizzo di quella posizione. Questo processo è chiamato allocazione nell’heap e talvolta è abbreviato semplicemente in allocazione (l’inserimento di valori sullo stack non è considerato allocazione). Poiché il puntatore all’heap ha una dimensione nota e fissa, è possibile archiviare il puntatore sullo stack, ma quando si desiderano i dati effettivi è necessario seguire il puntatore. Pensa di essere seduto in un ristorante. Quando entri, indichi il numero di persone nel tuo gruppo e il cameriere trova un tavolo vuoto adatto a tutti e ti conduce lì. Se qualcuno nel tuo gruppo arriva in ritardo, può chiedere dove sei seduto e trovarti.

Il push sullo stack è più veloce dell’allocazione nell’heap perché l’allocatore non deve mai cercare un posto dove archiviare i nuovi dati; quella posizione è sempre in cima allo stack. In confronto, l’allocazione dello spazio nell’heap richiede più lavoro perché l’allocatore deve prima trovare uno spazio sufficientemente grande per contenere i dati e quindi eseguire la contabilità per prepararsi all’allocazione successiva.

L’accesso ai dati nell’heap è più lento dell’accesso ai dati sullo stack perché è necessario leggere un puntatore sullo stack per poi “saltare” all’indirizzo di memoria nell’heap per accedere ai dati. I processori attuali sono più veloci se non “saltano” troppo in giro per la memoria. Continuando l’analogia, consideriamo un cameriere in un ristorante che prende ordini da molti tavoli. È più efficiente ricevere tutti gli ordini su un tavolo prima di passare al tavolo successivo. Prendere un ordine dal tavolo A, poi un ordine dal tavolo B, poi ancora uno da A e poi ancora uno da B sarebbe un processo molto più lento. Allo stesso modo, un processore può svolgere meglio il proprio lavoro se lavora su dati vicini ad altri dati (come sono sullo stack) piuttosto che più lontani (come possono essere nell’heap).

Quando il codice chiama una funzione, i valori passati alla funzione (inclusi, potenzialmente, puntatori ai dati nell’heap) e le variabili locali della funzione vengono inseriti sullo stack. Quando la funzione termina, tali valori vengono estratti, pop, sullo _stack.

Tenere traccia di quali parti del codice utilizzano quali dati nell’heap, ridurre al minimo la quantità di dati duplicati nell’heap e ripulire i dati inutilizzati nell’heap in modo da non esaurire la memoria sono tutti problemi che la ownership risolve. Una volta compresa la ownership, non sarà necessario pensare molto spesso allo stack e all’heap, ma comprendere che lo scopo principale della ownership è gestire i dati dell’heap può aiutare a capire perché funziona in questo modo.

Regole di Ownership

Per prima cosa, diamo un’occhiata alle regole di ownership, tenendole a mente mentre lavoriamo agli esempi che le illustrano:

  • Ogni valore in Rust ha un proprietario, owner.
  • Ci può essere un solo owner alla volta.
  • Quando l’owner esce dallo scope, il valore viene rilasciato.

Scope delle Variabili

Ora che abbiamo visto e imparato la sintassi di base di Rust, non includeremo tutto il codice fn main() { negli esempi, quindi se stai seguendo, assicurati di inserire manualmente i seguenti esempi all’interno di una funzione main. Di conseguenza, i nostri esempi saranno un po’ più concisi, permettendoci di concentrarci sui dettagli reali piuttosto che sul codice di base.

Come primo esempio di ownership, analizzeremo lo scope di alcune variabili. Lo scope è l’ambito all’interno di un programma nel quale un elemento è valido. Prendiamo la seguente variabile:

#![allow(unused)]
fn main() {
let s = "ciao";
}

La variabile s si riferisce a un letterale stringa, il cui valore è codificato nel testo del nostro programma. La variabile è valida dal momento in cui viene dichiarata fino alla fine dello scope corrente. Il Listato 4-1 mostra un programma con commenti che annotano i punti in cui la variabile s sarebbe valida (in scope).

fn main() {
    {                      // `s` non è valida qui, perché non ancora dichiarata
        let s = "hello";   // `s` è valida da questo punto in poi

        // fai cose con `s`
    }                      // questo scope è finito, e `s` non è più valida
}
Listato 4-1: Una variabile e lo scope in cui è valida

In altre parole, ci sono due momenti importanti:

  • Quando s entra nello scope, è valida;
  • Rimane valida fino a quando non esce dallo scope.

A questo punto, la relazione tra scope e validità delle variabili è simile a quella di altri linguaggi di programmazione. Ora ci baseremo su questa comprensione introducendo il type String.

Il Type String

Per illustrare le regole di ownership, abbiamo bisogno di un tipo di dati più complesso di quelli trattati nel Capitolo 3. I type trattati in precedenza hanno dimensioni note, possono essere inseriti e estratti sullo stack quando il loro scope è terminato, possono essere rapidamente copiati per creare una nuova istanza indipendente se un’altra parte del codice deve utilizzare lo stesso valore in uno scope diverso. Ma vogliamo esaminare i dati archiviati nell’heap e capire come Rust sa quando ripulire la memoria che quei dati usavano quando non serve più, e il type String è un ottimo esempio da cui partire.

Ci concentreremo sulle parti di String che riguardano la ownership. Questi aspetti si applicano anche ad altri type di dati complessi, siano essi forniti dalla libreria standard o creati dall’utente. Parleremo di String oltre l’aspetto della ownership nel Capitolo 8.

Abbiamo già visto i letterali stringa, in cui un valore stringa è codificato nel nostro programma. I letterali stringa sono convenienti, ma non sono adatti a tutte le situazioni in cui potremmo voler utilizzare del testo. Uno dei motivi è che sono immutabili. Un altro è che non tutti i valori di stringa possono essere conosciuti quando scriviamo il nostro codice: ad esempio, cosa succederebbe se volessimo prendere l’input dell’utente e memorizzarlo? È per queste situazioni che Rust ha un il type String. Questo type gestisce i dati allocati nell’heap e come tale è in grado di memorizzare una quantità di testo a noi sconosciuta in fase di compilazione. Puoi creare un type String partendo da un letterale stringa utilizzando la funzione from, in questo modo:

#![allow(unused)]
fn main() {
let s = String::from("ciao");
}

L’operatore double colon (doppio - due punti) :: ci permette di integrare questa particolare funzione from nel type String piuttosto che usare un nome come string_from. Parleremo di questa sintassi nella sezione “Sintassi dei metodi” del Capitolo 5 e quando parleremo di come organizzare la nomenclatura nei moduli in “Percorsi per fare riferimento a un elemento nell’albero dei moduli” nel Capitolo 7.

Questo tipo di stringa può essere mutata:

fn main() {
    let mut s = String::from("hello");

    s.push_str(", world!"); // push_str() aggiunge un letterale a una String

    println!("{s}");        // verrà stampato `hello, world!`
}

Allora, qual è la differenza? Perché String può essere mutata ma i letterali no? La differenza sta nel modo in cui questi due type vengono gestiti in memoria.

Memoria e Allocazione

Nel caso di un letterale stringa, conosciamo il contenuto al momento della compilazione, quindi il testo è codificato direttamente nell’eseguibile finale. Per questo motivo i letterali stringa sono veloci ed efficienti. Ma queste proprietà derivano solo dall’immutabilità del letterale stringa. Sfortunatamente, non possiamo inserire una porzione di memoria indefinita nel binario per ogni pezzo di testo la cui dimensione è sconosciuta al momento della compilazione e la cui dimensione potrebbe cambiare durante l’esecuzione del programma.

Con il type String, per supportare una porzione di testo mutabile e espandibile, dobbiamo allocare una quantità di memoria nell’heap, sconosciuta in fase di compilazione, per contenere il contenuto. Questo significa che:

  • La memoria deve essere richiesta all’allocatore di memoria in fase di esecuzione.
  • Abbiamo bisogno di un modo per restituire questa memoria all’allocatore quando abbiamo finito con la nostra String.

La prima parte la facciamo noi: quando chiamiamo String::from, la sua implementazione richiede la memoria di cui ha bisogno. Questo è praticamente universale nei linguaggi di programmazione.

Tuttavia, la seconda parte è diversa. Nei linguaggi con un garbage collector (GC), il GC tiene traccia e ripulisce la memoria che non viene più utilizzata e non abbiamo bisogno di pensarci. Nella maggior parte dei linguaggi senza GC, è nostra responsabilità identificare quando la memoria non viene più utilizzata e chiamare il codice per de-allocarla esplicitamente, proprio come abbiamo fatto per richiederla. Farlo correttamente è stato storicamente un difficile problema di programmazione. Se ce lo dimentichiamo, sprecheremo memoria. Se lo facciamo troppo presto, avremo una variabile non valida. Se lo facciamo due volte, anche questo è un bug. Dobbiamo accoppiare esattamente un’allocazione con esattamente una de-allocazione (o rilascio, liberazione).

Rust prende una strada diversa: la memoria viene rilasciata automaticamente una volta che la variabile che la possiede esce dallo scope. Ecco una versione del nostro esempio sullo scope che utilizza una String invece di un letterale stringa:

fn main() {
    {
        let s = String::from("hello"); // `s` è valida da questo punto in poi

        // fai cose con `s`
    }   // questo scope è finito, e `s` non è più valida
}

C’è un punto naturale in cui possiamo rilasciare la memoria di cui la nostra String ha bisogno all’allocatore: quando s esce dallo scope. Quando una variabile esce dallo scope, Rust chiama per noi una funzione speciale. Questa funzione si chiama drop, ed è dove l’autore di String può inserire il codice per rilasciare la memoria. Rust chiama drop automaticamente alla parentesi graffa di chiusura.

Nota: in C++, questo schema di de-allocazione delle risorse alla fine del ciclo di vita di un elemento è talvolta chiamato Resource Acquisition Is Initialization (RAII). La funzione drop di Rust ti sarà familiare se hai usato gli schemi RAII.

Questo schema ha un profondo impatto sul modo in cui viene scritto il codice Rust. Può sembrare semplice in questo momento, ma il comportamento del codice può essere inaspettato in situazioni più complicate quando vogliamo che più variabili utilizzino i dati che abbiamo allocato nell’heap. Esploriamo ora alcune di queste situazioni.

Interazione tra Variabili e Dati con Move

In Rust, più variabili possono interagire con gli stessi dati in modi diversi. Il Listato 4-2 mostra un esempio che utilizza un integer.

fn main() {
    let x = 5;
    let y = x;
}
Listato 4-2: Assegnazione del valore integer della variabile x a y

Probabilmente possiamo indovinare cosa sta facendo: “Associa il valore 5 a x; quindi crea una copia del valore in x e associala a y.” Ora abbiamo due variabili, x e y, ed entrambe uguali a 5. Questo è effettivamente ciò che sta accadendo. Poiché gli integer sono valori semplici con una dimensione fissa e nota, questi due valori 5 vengono immessi sullo stack.

Ora diamo un’occhiata alla versione con String:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
}

Sembra molto simile, quindi potremmo pensare che il funzionamento sia lo stesso: cioè che la seconda riga faccia una copia del valore in s1 e lo assegni a s2. Ma non è esattamente quello che succede.

Nella Figura 4-1 diamo un’occhiata sotto le coperte per vedere com’è in realtà una String. Una String è composta da tre parti, mostrate a sinistra: un puntatore (ptr) alla memoria che contiene il contenuto della stringa, una lunghezza (len) e una capienza (capacity). Questo gruppo di dati è memorizzato sullo stack. A destra c’è la memoria nell’heap che contiene il contenuto.

Due
tabelle: la prima tabella contiene la rappresentazione di s1 nello stack,
composta dalla lunghezza (5), capienza (5), e un puntatore al primo valore della
seconda tabella. La seconda tabella contiene una rappresentazione del contenuto
della stringa nell’heap, byte per byte.

Figura 4-1: La representazione in memoria di una String con valore "hello" assegnato a s1

La lunghezza è la quantità di memoria, in byte, utilizzata attualmente dal contenuto della String. La capienza è la quantità totale di memoria, in byte, che String ha ricevuto dall’allocatore. La differenza tra lunghezza e capacità è importante, ma non in questo contesto, quindi per ora va bene ignorare la capienza.

Quando assegniamo s1 a s2, i dati String vengono copiati, ovvero copiamo il puntatore, la lunghezza e la capienza presenti sullo stack. Non copiamo i dati nell’heap a cui fa riferimento il puntatore. In altre parole, la rappresentazione dei dati in memoria è simile alla Figura 4-2.

Tre
tabelle: tabella s1 e s2 rappresentano quelle stringhe nello stack,
indipendentemente, ed entrambe puntano agli stessi dati della stringa
nell’heap.

Figura 4-2: La rappresentazione in memoria della variabile s2 che contiene una copia del puntatore, lunghezza e capienza di s1

La rappresentazione non assomiglia alla Figura 4-3, che è l’aspetto che avrebbe la memoria se Rust copiasse anche i dati dell’heap. Se Rust facesse così, l’operazione s2 = s1 potrebbe diventare molto dispendiosa in termini prestazionali e di memoria qualora i dati nell’heap fossero di grandi dimensioni.

Quattro
tabelle: due tabelle rappresentano i dati sullo stack di s1 e s2, ognuna delle
quali punta alla propria copia di dati nell’heap.

Figura 4-3: Un’altra possibilità di come potrebbe essere s2 = s1 se Rust copiasse anche i dati dell’heap

In precedenza, abbiamo detto che quando una variabile esce dallo scope, Rust chiama automaticamente la funzione drop e ripulisce la memoria nell’heap di quella variabile. Ma la Figura 4-2 mostra entrambi i puntatori di dati che puntano alla stessa posizione. Questo è un problema: quando s2 e s1 escono dallo scope, entrambe tenteranno di de-allocare la stessa memoria. Questo è noto come errore da doppia de-allocazione (double free error in inglese) ed è uno dei bug di sicurezza della memoria menzionati in precedenza. Rilasciare la memoria due volte può portare a corruzione di memoria, che può potenzialmente esporre il programma a vulnerabilità di sicurezza.

Per garantire la sicurezza della memoria, dopo la riga let s2 = s1;, Rust considera s1 come non più valido. Pertanto, Rust non ha bisogno di de-allocare nulla quando s1 esce dallo scope. Controlla cosa succede quando provi a usare s1 dopo che s2 è stato creato: non funzionerà:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;

    println!("{s1}, world!");
}

Otterrai un errore di questo tipo perché Rust ti impedisce di utilizzare il riferimento invalidato:

$ cargo run
   Compiling ownership v0.1.0 (file:///progetti/ownership)
error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:6:16
  |
3 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
4 |     let s2 = s1;
  |              -- value moved here
5 |
6 |     println!("{s1}, world!");
  |                ^^ value borrowed here after move
  |
  = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
  |
4 |     let s2 = s1.clone();
  |                ++++++++

For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

Se hai sentito i termini copia superficiale e copia profonda mentre lavoravi con altri linguaggi, il concetto di copiare il puntatore, la lunghezza e la capacità senza copiare i dati probabilmente ti sembrerà simile a una copia superficiale. Ma poiché Rust invalida anche la prima variabile, invece di essere chiamata copia superficiale, questa operazione è nota come move (spostamento). In questo esempio, diremmo che s1 è stata spostata in s2. Quindi, ciò che accade in realtà è mostrato nella Figura 4-4.

Tre
tabelle: tabelle s1 e s2 rappresentano rispettivamente le quelle stringhe sullo
stack, ed entrambe puntano alla medesima stringa nell’heap. Tabella s1 è scurita
perché s1 non è più valida; solo s2 può essere usata per accedere ai dati
nell’heap.

Figura 4-4: La rappresentazione in memoria dopo che s1 è resa non valida

Questo risolve il nostro problema! Con la sola s2 valida, quando essa uscirà dallo scope, solo lei rilascerà la memoria e il gioco è fatto.

Inoltre, c’è una scelta progettuale implicita in questo: Rust non creerà mai automaticamente copie “profonde” dei tuoi dati. Pertanto, si può presupporre che qualsiasi copia automatica sia poco dispendiosa in termini prestazionali e di memoria.

Scope e Assegnazione

L’opposto di ciò è vero anche per la relazione tra scope, ownership e memoria rilasciata tramite la funzione drop. Quando assegni un valore completamente nuovo a una variabile esistente, Rust chiamerà drop e libererà immediatamente la memoria del valore originale. Considera questo codice, ad esempio:

fn main() {
    let mut s = String::from("hello");
    s = String::from("ciao");

    println!("{s}, world!");
}

Inizialmente dichiariamo una variabile s e la associamo a una String con il valore "hello". Poi creiamo immediatamente una nuova String con il valore "ciao" e la assegniamo a s. A questo punto, non c’è più nulla che faccia riferimento al valore originale nell’heap. La Figura 4-5 mostra i dati sullo stack e nell’heap al momento:

Una tabella
rappresenta la stringa sullo stack, che punta ai dati della stringa (ciao)
nell’heap, con i dati della stringa originale (hello) scuriti perché non più
accessibili.

Figura 4-5: La rappresentazione in memoria dopo che il primo valore è completamente sostituito.

La stringa originale esce così immediatamente dallo scope. Rust eseguirà la funzione drop su di essa e la sua memoria verrà rilasciata immediatamente. Quando stamperemo il valore alla fine, sarà "ciao, world!".

Interazione tra Variabili e Dati con Clone

Se vogliamo effettivamente duplicare i dati nell’heap della String, e non solo i dati sullo stack, possiamo utilizzare un metodo comune chiamato clone. Parleremo della sintassi dei metodi nel Capitolo 5, ma dato che i metodi sono una caratteristica comune a molti linguaggi di programmazione, probabilmente li hai già visti.

Ecco un esempio del metodo clone in azione:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();

    println!("s1 = {s1}, s2 = {s2}");
}

Questo funziona benissimo e produce esplicitamente il comportamento mostrato nella Figura 4-3, in cui i anche i dati dell’heap vengono duplicati.

Quando vedi una chiamata a clone, sai che viene eseguito del codice arbitrario e che questo potrebbe essere dispendioso. È un indicatore visivo del fatto che sta succedendo qualcosa di diverso.

Duplicare Dati Sullo Stack

C’è un’altra peculiarità di cui non abbiamo ancora parlato: questo codice che utilizza gli integer, in parte mostrato nel Listato 4-2, funziona ed è valido

fn main() {
    let x = 5;
    let y = x;

    println!("x = {x}, y = {y}");
}

Ma questo codice sembra contraddire ciò che abbiamo appena imparato: non abbiamo una chiamata a clone, ma x è ancora valido e non è stato spostato in y.

Il motivo è che i type come gli integer che hanno una dimensione nota in fase di compilazione vengono archiviati interamente sullo stack, quindi le copie dei valori effettivi sono veloci da creare. Ciò significa che non c’è motivo per cui vorremmo impedire che x sia valido dopo aver creato la variabile y. In altre parole, qui non c’è differenza tra copia profonda e superficiale, quindi chiamare clone non farebbe nulla di diverso dalla solita copia superficiale e possiamo tralasciarlo.

Rust ha un’annotazione speciale chiamata tratto Copy che possiamo appiccicare sui type memorizzati sullo stack, come lo sono gli integer (parleremo meglio dei tratti nel Capitolo 10). Se un type implementa il tratto Copy, le variabili che lo utilizzano non si spostano, ma vengono semplicemente copiate, rendendole ancora valide dopo l’assegnazione ad un’altra variabile.

Rust non ci permette di annotare un type con Copy se il type, o una qualsiasi delle sue parti, ha implementato il tratto Drop. Se il type ha bisogno che accada qualcosa di speciale quando il valore esce dallo scope e aggiungiamo l’annotazione Copy a quel type, otterremo un errore in fase di compilazione. Per sapere come aggiungere l’annotazione Copy al tuo type, consulta “Tratti derivabili” nell’Appendice C.

Quindi, quali type implementano il tratto Copy? Puoi controllare la documentazione del type in questione per esserne sicuro, ma come regola generale, qualsiasi gruppo di valori scalari semplici può implementare Copy e niente che richieda l’allocazione o che sia una qualche forma di risorsa può implementare Copy:

Ecco alcuni dei type che implementano Copy:

  • Tutti i type integer, come u32.
  • Il type booleano, bool, con i valori true e false.
  • Tutti i type in virgola mobile, come f64.
  • Il type carattere, char.
  • Le tuple, se contengono solo type che implementano Copy. Ad esempio, (i32, i32) implementa Copy, ma (i32, String) no.

Ownership e Funzioni

I meccanismi che regolano il passaggio di un valore a una funzione sono simili a quelli dell’assegnazione di un valore a una variabile. Passando una variabile a una funzione, questa viene spostata o copiata, proprio come fa l’assegnazione. Il Listato 4-3 contiene un esempio con alcune annotazioni che mostrano dove le variabili entrano ed escono dallo scope.

File: src/main.rs
fn main() {
    let s = String::from("hello"); // `s` entra nello scope

    prende_ownership(s);           // il valore di `s` viene spostato nella funzione...
                                   // ... e quindi qui smette di esser valido

    let x = 5;                     // `x` entra nello scope

    duplica(x);                    // Siccome i32 implementa il tratto Copy,
                                   // `x` NON viene spostato nella funzione,
                                   // quindi dopo può ancora essere usata.

}   // Qui, `x` esce dallo scope, ed anche `s`. Tuttavia, siccome il valore di `s`
    // era stato spostato, non succede nulla di particolare.

fn prende_ownership(una_stringa: String) { // `una_stringa` entra nello scope
    println!("{una_stringa}");
}   // Qui, `una_stringa` esce dallo scope e `drop` viene chiamato.
    // La memoria corrispondente viene rilasciata.

fn duplica(un_integer: i32) { // `un_integer` entra nello scope
    println!("{un_integer}");
}   // Qui, `un_integer` esce dallo scope. Non succede nulla di particolare.
Listato 4-3: Funzioni con ownership e scope annotate

Se provassimo a usare s dopo la chiamata a prende_ownership, Rust segnalerebbe un errore in fase di compilazione. Questi controlli statici ci proteggono dagli errori. Prova ad aggiungere del codice a main che usi s e x per sperimentare dove puoi usarli e dove le regole di ownership te lo impediscono.

Valori di Ritorno e Scope

I valori di ritorno possono anch’essi trasferire la ownership. Il Listato 4-4 mostra un esempio di funzione che restituisce un valore, con annotazioni simili a quelle del Listato 4-3.

File: src/main.rs
fn main() {
    let s1 = cede_ownership();         // `cede_ownership` sposta il proprio
                                       // valore di ritorno in `s1`

    let s2 = String::from("hello");    // `s2` entra in scope

    let s3 = prende_e_restituisce(s2); // `s2` viene spostata in
                                       // `prende_e_restituisce`, che a sua
                                       // volta sposta il proprio valore
                                       // di ritorno in `s3`

} // Qui, `s3` esce dallo scope e viene cancellata con `drop`. `s2` era stata spostata
  // e quindi non succede nulla. `s1` viene cancellata con `drop` anch'essa.

fn cede_ownership() -> String {   // `cede_ownership` spostera il proprio valore di
                                  // ritorno alla funzione che l'ha chiamata

    let una_stringa = String::from("yours"); // `una_stringa` entra in scope

    una_stringa                     // `una_stringa` viene ritornata e spostata
                                    // alla funzione chiamante
}

// Questa funzione prende una String e ritorna una String.
fn prende_e_restituisce(altra_stringa: String) -> String {
                    // `altra_stringa` entra in scope

    altra_stringa   // `altra_stringa` viene ritornata
                    // e spostata alla funzione chiamante
}
Listato 4-4: Trasferimento di ownership nei valori di ritorno

La ownership di una variabile segue ogni volta lo stesso schema: assegnare un valore a un’altra variabile la sposta. Quando una variabile che include dati nell’heap esce dallo scope, il valore verrà cancellato da drop a meno che la ownership dei dati non sia stata spostata ad un’altra variabile.

Anche se funziona, prendere e cedere la ownership con ogni funzione è un po’ faticoso. Cosa succede se vogliamo consentire a una funzione di utilizzare un valore ma non di prenderne la ownership? È piuttosto fastidioso che tutto ciò che passiamo debba anche essere restituito se vogliamo usarlo di nuovo, oltre a tutte le varie elaborazioni sui dati che la funzione esegue e che magari è necessario ritornare pure quelle.

Rust ci permette di ritornare più valori utilizzando una tupla, come mostrato nel Listato 4-5

File: src/main.rs
fn main() {
    let s1 = String::from("ciao");

    let (s2, lung) = calcola_lunghezza(s1);

    println!("La lunghezza di '{s2}' è {lung}.");
}

fn calcola_lunghezza(s: String) -> (String, usize) {
    let lunghezza = s.len(); // len() restituisce la lunghezza di una String

    (s, lunghezza)
}
Listato 4-5: Restituzione ownership dei parametri

Ma questa è una procedura inutilmente complessa e richiede molto lavoro per un concetto che dovrebbe essere comune. Fortunatamente per noi, Rust ha una funzionalità che consente di utilizzare un valore senza trasferirne la ownership, chiamata riferimento (reference in inglese).