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

Trattare i Puntatori Intelligenti Come Normali Reference

L’implementazione del trait Deref consente di personalizzare il comportamento dell’operatore di de-referenziazione (dereference operator) * (da non confondere con l’operatore di moltiplicazione o glob). Implementando Deref in modo tale che un puntatore intelligente possa essere trattato come un normale reference, è possibile scrivere codice che opera sui reference e utilizzarlo anche con i puntatori intelligenti.

Vediamo prima come funziona l’operatore di de-referenziazione con i normali reference. Poi proveremo a definire un type personalizzato che si comporti come Box<T> e vedremo perché l’operatore di de-referenziazione non funziona come un reference sul nostro nuovo type che abbiamo definito. Esploreremo come l’implementazione del trait Deref consenta ai puntatori intelligenti di funzionare in modo simile ai reference. Infine, esamineremo la funzionalità di Rust di de-referenziazione forzata (deref coercion) e come ci consenta di lavorare sia con i reference che con i puntatori intelligenti.

Seguire il Reference al Valore

Un normale reference è un tipo di puntatore, un modo per pensare a un puntatore è immaginare una freccia che punta verso un valore memorizzato altrove. Nel Listato 15-6, creiamo un reference a un valore i32 e poi utilizziamo l’operatore di de-referenziazione per seguire il riferimento al valore.

File: src/main.rs
fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
Listato 15-6: Utilizzo dell’operatore di de-referenziazione per seguire un riferimento a un valore i32

La variabile x contiene un valore i32 5. Impostiamo y uguale a un reference a x. Possiamo asserire che x è uguale a 5. Tuttavia, se vogliamo fare un’asserzione sul valore in y, dobbiamo usare *y per seguire il reference al valore a cui punta (da qui de-referenziazione) in modo che il compilatore possa confrontare il valore effettivo. Una volta de-referenziato y, abbiamo accesso al valore intero a cui punta y, che possiamo confrontare con 5.

Se provassimo a scrivere assert_eq!(5, y);, otterremmo questo errore di compilazione:

$ cargo run
   Compiling esempio-deref v0.1.0 (file:///progetti/esempio-deref)
error[E0277]: can't compare `{integer}` with `&{integer}`
 --> src/main.rs:6:5
  |
6 |     assert_eq!(5, y);
  |     ^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
  |
  = help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}`
  = note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)

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

Il confronto tra un numero e un reference a un numero non è consentito perché sono diversi. Dobbiamo usare l’operatore di de-referenziazione per seguire il reference al valore a cui punta.

Utilizzare Box<T> Come Reference

Possiamo riscrivere il codice nel Listato 15-6 per utilizzare Box<T> invece di un reference; l’operatore di de-referenziazione utilizzato su Box<T> nel Listato 15-7 funziona allo stesso modo dell’operatore di de-referenziazione utilizzato sul reference nel Listato 15-6.

File: src/main.rs
fn main() {
    let x = 5;
    let y = Box::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
Listato 15-7: Utilizzare l’operatore di de-referenziazione su una Box<i32>

La differenza principale tra il Listato 15-7 e il Listato 15-6 è che qui impostiamo y come un’istanza di una box che punta a un valore copiato di x anziché come un reference che punta al valore di x. Nell’ultima asserzione, possiamo usare l’operatore di de-referenziazione per seguire il puntatore della box nello stesso modo in cui facevamo quando y era un reference. Successivamente, esploreremo le peculiarità di Box<T> che ci consentono di utilizzare l’operatore di de-referenziazione definendo un nostro type di box.

Definire il Nostro Puntatore Intelligente

Creiamo un type incapsulatore, simile al type Box<T> fornito dalla libreria standard, per sperimentare come i tipi di puntatore intelligente si comportino diversamente dai normali reference. Poi vedremo come aggiungere la possibilità di utilizzare l’operatore di de-referenziazione.

Nota: c’è una grande differenza tra il type MioBox<T> che stiamo per creare e il vero Box<T>: la nostra versione non memorizzerà i dati nell’heap. Ci stiamo concentrando su Deref, quindi dove vengono effettivamente memorizzati i dati è meno importante del comportamento “simile” a un puntatore.

Il type Box<T> è essenzialmente definito come una struct tupla con un elemento, quindi il Listato 15-8 definisce un type MioBox<T> allo stesso modo. Definiremo anche una funzione new che corrisponda alla funzione new definita su Box<T>.

File: src/main.rs
struct MioBox<T>(T);

impl<T> MioBox<T> {
    fn new(x: T) -> MioBox<T> {
        MioBox(x)
    }
}

fn main() {}
Listato 15-8: Definizione di un type MioBox<T>

Definiamo una struct denominata MioBox e dichiariamo un parametro generico T perché vogliamo che il nostro type contenga valori di qualsiasi type. Il type MioBox è una struct tupla con un elemento di type T. La funzione MioBox::new accetta un parametro di type T e restituisce un’istanza di MioBox che contiene il valore passato.

Proviamo ad aggiungere la funzione main del Listato 15-7 al Listato 15-8 e modificarla in modo che utilizzi il type MioBox<T> che abbiamo definito invece di Box<T>. Il codice nel Listato 15-9 non verrà compilato perché Rust non sa come de-referenziare MioBox.

File: src/main.rs
struct MioBox<T>(T);

impl<T> MioBox<T> {
    fn new(x: T) -> MioBox<T> {
        MioBox(x)
    }
}

fn main() {
    let x = 5;
    let y = MioBox::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
Listato 15-9: Tentativo di utilizzare MioBox<T> nello stesso modo in cui abbiamo utilizzato i reference e Box<T>

Ecco l’errore di compilazione risultante:

$ cargo run
   Compiling esempio-deref v0.1.0 (file:///progetti/esempio-deref)
error[E0614]: type `MioBox<{integer}>` cannot be dereferenced
  --> src/main.rs:15:19
   |
15 |     assert_eq!(5, *y);
   |                   ^^ can't be dereferenced

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

Il nostro type MioBox<T> non può essere de-referenziato perché non abbiamo implementato tale possibilità sul nostro type. Per abilitare la de-referenziazione con l’operatore *, implementiamo il trait Deref.

Implementare il Trait Deref

Come discusso in “Implementare un Trait su un Type nel Capitolo 10, per implementare un trait dobbiamo fornire le implementazioni per i metodi richiesti dal trait. Il trait Deref, fornito dalla libreria standard, richiede l’implementazione di un metodo chiamato deref che prende in prestito self e restituisce un reference ai dati interni. Il Listato 15-10 contiene un’implementazione di Deref da aggiungere alla definizione di MioBox<T>.

File: src/main.rs
use std::ops::Deref;

impl<T> Deref for MioBox<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

struct MioBox<T>(T);

impl<T> MioBox<T> {
    fn new(x: T) -> MioBox<T> {
        MioBox(x)
    }
}

fn main() {
    let x = 5;
    let y = MioBox::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
Listato 15-10: Implementazione di Deref su MioBox<T>

La sintassi type Target = T; definisce un type associato che il trait Deref può utilizzare. I type associati rappresentano un modo leggermente diverso di dichiarare un parametro generico, ma per ora non è necessario preoccuparsene; li tratteremo più dettagliatamente nel Capitolo 20.

Nel corpo del metodo deref inseriamo &self.0 in modo che deref restituisca un reference al valore a cui vogliamo accedere con l’operatore *; come detto in “Creare Type Diversi con Struct Tupla ” nel Capitolo 5, .0 accede al primo valore in una struct tupla. La funzione main nel Listato 15-10 che chiama * sul valore MioBox<T> ora si compila e le asserzioni vengono verificate!

Senza il trait Deref, il compilatore può de-referenziare solo i reference &. Il metodo deref consente al compilatore di accettare un valore di qualsiasi type che implementi Deref e chiamare il metodo deref per ottenere un reference & che sa come de-referenziare.

Quando abbiamo inserito *y nel Listato 15-10, dietro le quinte Rust ha effettivamente eseguito questo codice:

*(y.deref())

Rust sostituisce l’operatore * con una chiamata al metodo deref e poi un semplice de-referenziamento, così non dobbiamo pensare se sia necessario o meno chiamare il metodo deref. Questa funzionalità di Rust ci permette di scrivere codice che funziona nello stesso modo indipendentemente dal fatto che abbiamo un normale reference o un type che implementa Deref.

Il motivo per cui il metodo deref restituisce un reference a un valore, e il fatto che il semplice de-referenziamento al di fuori delle parentesi in *(y.deref()) sia ancora necessario, ha a che fare con il sistema di ownership. Se il metodo deref restituisse il valore direttamente invece di un reference al valore, il valore verrebbe spostato fuori da self. Non vogliamo prendere la ownership del valore interno di MioBox<T> in questo caso, né nella maggior parte dei casi in cui utilizziamo l’operatore di de-referenziazione.

Nota che l’operatore * viene sostituito con una chiamata al metodo deref e poi con una chiamata all’operatore * una sola volta, ogni volta che utilizziamo * nel nostro codice. Poiché la sostituzione dell’operatore * non è ricorsiva all’infinito, otteniamo dati di type i32, che corrispondono al 5 in assert_eq! nel Listato 15-9.

Usare la De-Referenziazione Forzata in Funzioni e Metodi

La deref coercion converte un reference a un type che implementa il trait Deref in un reference a un altro type. Ad esempio, la de-referenziazione forzata può convertire &String in &str perché String implementa il trait Deref in modo tale da restituire &str. La de-referenziazione forzata è una funzionalità che Rust applica agli argomenti di funzioni e metodi e funziona solo sui type che implementano il trait Deref. Avviene automaticamente quando passiamo un reference al valore di un type specifico come argomento a una funzione o a un metodo che non corrisponde al type di parametro nella definizione della funzione o del metodo. Una sequenza di chiamate al metodo deref converte il type fornito nel type richiesto dal parametro.

La deref coercion è stata aggiunta a Rust in modo che i programmatori che scrivono chiamate di funzioni e metodi non debbano esplicitare troppo spesso i reference o i dereference con & e *. La funzionalità di de-referenziazione forzata ci consente anche di scrivere più codice che può funzionare sia per reference che per puntatori intelligenti.

Per vedere la deref coercion in azione, utilizziamo il type MioBox<T> definito nel Listato 15-8 e l’implementazione di Deref aggiunta nel Listato 15-10. Il Listato 15-11 mostra la definizione di una funzione che ha un parametro di type slice stringa.

File: src/main.rs
fn ciao(nome: &str) {
    println!("Ciao, {nome}!");
}

fn main() {}
Listato 15-11: Una funzione ciao che ha il parametro nome di type &str

Possiamo chiamare la funzione ciao con un parametro di type slice stringa come argomento, ad esempio ciao("Rust");. La deref coercion consente di chiamare ciao con un reference a un valore di type MioBox<String>, come mostrato nel Listato 15-12.

File: src/main.rs
use std::ops::Deref;

impl<T> Deref for MioBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}

struct MioBox<T>(T);

impl<T> MioBox<T> {
    fn new(x: T) -> MioBox<T> {
        MioBox(x)
    }
}

fn ciao(nome: &str) {
    println!("Ciao, {nome}!");
}

fn main() {
    let m = MioBox::new(String::from("Rust"));
    ciao(&m);
}
Listato 15-12: Chiamata di ciao con un reference a un valore MioBox<String>, che funziona grazie alla deref coercion

Qui chiamiamo la funzione ciao con l’argomento &m, che è un reference a un valore MioBox<String>. Poiché abbiamo implementato il trait Deref su MioBox<T> nel Listato 15-10, Rust può trasformare &MioBox<String> in &String chiamando deref. La libreria standard fornisce un’implementazione di Deref su String che restituisce una slice di stringa, ed è presente nella documentazione API per Deref. Rust chiama nuovamente deref per trasformare &String in &str, che corrisponde alla definizione della funzione ciao.

Se Rust non implementasse la de-referenziazione forzata, dovremmo scrivere il codice nel Listato 15-13 invece del codice nel Listato 15-12 per chiamare ciao con un valore di tipo &MioBox<String>.

File: src/main.rs
use std::ops::Deref;

impl<T> Deref for MioBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}

struct MioBox<T>(T);

impl<T> MioBox<T> {
    fn new(x: T) -> MioBox<T> {
        MioBox(x)
    }
}

fn ciao(nome: &str) {
    println!("Ciao, {nome}!");
}

fn main() {
    let m = MioBox::new(String::from("Rust"));
    ciao(&(*m)[..]);
}
Listato 15-13: Il codice che dovremmo scrivere se Rust non avesse la de-referenziazione forzata

(*m) de-referenzia MioBox<String> in una String. Quindi & e [..] prendono una slice di String che è uguale all’intera stringa per corrispondere alla firma di ciao. Questo codice senza deref coercion con tutti questi simboli coinvolti è più difficile da leggere, scrivere e comprendere. La deref consente a Rust di gestire automaticamente queste conversioni.

Quando il trait Deref è definito per i type coinvolti, Rust analizzerà i type e utilizzerà Deref::deref tutte le volte necessarie per ottenere un reference che corrisponda al type del parametro. Il numero di volte in cui Deref::deref deve essere inserito viene risolto in fase di compilazione, quindi non ci sono penalità prestazionali in fase di esecuzione per aver sfruttato la deref coercion!

Gestire la De-referenziazione Forzata con Reference Mutabili

Analogamente a come si usa il trait Deref per sovrascrivere l’operatore * sui reference immutabili, è possibile usare il trait DerefMut per sovrascrivere l’operatore * sui reference mutabili.

Rust esegue la deref coercion quando trova type e implementazioni di trait in tre casi:

  1. Da &T a &U quando T: Deref<Target=U>
  2. Da &mut T a &mut U quando T: DerefMut<Target=U>
  3. Da &mut T a &U quando T: Deref<Target=U>

I primi due casi sono gli stessi, tranne per il fatto che il secondo implementa la mutabilità. Il primo caso afferma che se si ha un &T e T implementa Deref a un type U, è possibile ottenere un &U in modo trasparente. Il secondo caso afferma che la stessa de-referenziazione forzata avviene per i reference mutabili.

Il terzo caso è più complicato: Rust convertirà anche un reference mutabile a uno immutabile. Ma il contrario non è possibile: i reference immutabili non verranno mai convertirti in reference mutabili. A causa delle regole di prestito, se si ha un reference mutabile, quel reference mutabile deve essere l’unico reference a quei dati (altrimenti, il programma non verrebbe compilato). La conversione di un reference mutabile in un reference immutabile non violerà mai le regole di prestito. La conversione di un reference immutabile in un reference mutabile richiederebbe che il reference immutabile iniziale sia l’unico reference immutabile a quei dati, ma le regole di prestito non lo garantiscono. Pertanto, Rust non può dare per scontato che sia possibile convertire un reference immutabile in un reference mutabile.