Shopping cart

0

Cart

  • 0 item

Nessun prodotto nel carrello.

All categories

SE VUOI PRENDERE LA CERTIFICAZIONE PER QUESTO CORSO CLICCA QUI

Gestione dei Puntatori Intelligenti Come Riferimenti Normali con il Trait Deref

Implementare il trait Deref ti permette di personalizzare il comportamento dell’operatore di dereferenziazione * (da non confondere con l’operatore di moltiplicazione o l’operatore glob). Implementando Deref in modo che un puntatore intelligente possa essere trattato come un riferimento normale, puoi scrivere codice che funziona su riferimenti e usare quel codice anche con puntatori intelligenti.

Vediamo prima come funziona l’operatore di dereferenziazione con i riferimenti normali. Poi cercheremo di definire un tipo personalizzato che si comporti come Box<T>, e vediamo perché l’operatore di dereferenziazione non funziona come un riferimento sul nostro tipo appena definito. Esploreremo come l’implementazione del trait Deref renda possibile per i puntatori intelligenti funzionare in modi simili ai riferimenti. Poi esamineremo la funzionalità di coercizione del deref di Rust e come ci permette di lavorare sia con riferimenti che con puntatori intelligenti.

vbnet

Nota: c'è una grande differenza tra il tipo MyBox<T> che stiamo per costruire e il vero Box<T>: la nostra versione non memorizzerà i suoi dati sull'heap. Ci stiamo concentrando su questo esempio su Deref, quindi dove i dati sono effettivamente memorizzati è meno importante del comportamento simile a un puntatore.

Seguire il Puntatore al Valore

Un riferimento normale è un tipo di puntatore, e un modo per pensare a un puntatore è come una freccia verso un valore memorizzato altrove. Nel Listato 15-6, creiamo un riferimento a un valore i32 e poi usiamo l’operatore di dereferenziazione per seguire il riferimento al valore:

rust

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

assert_eq!(5, x);
assert_eq!(5, *y);
}

La variabile x contiene un valore i32 5. Impostiamo y uguale a un riferimento a x. Possiamo affermare che x è uguale a 5. Tuttavia, se vogliamo fare un’affermazione sul valore in y, dobbiamo usare *y per seguire il riferimento al valore a cui punta (quindi dereferenziazione) in modo che il compilatore possa confrontare il valore effettivo. Una volta dereferenziato y, abbiamo accesso al valore intero a cui punta y che possiamo confrontare con 5.

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

rust

$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
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}`
= help: the following other types implement trait `PartialEq<Rhs>`:
f32
f64
i128
i16
i32
i64
i8
isize
and 6 others
= note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)
Per maggiori informazioni su questo errore, prova `rustc --explain E0277`.
errore: non è possibile compilare `deref-example` a causa di un errore precedente Confrontare un numero e un riferimento a un numero non è consentito perché sono tipi diversi. Dobbiamo usare l'operatore di dereferenziazione per seguire il riferimento al valore a cui punta.
Utilizzo di Box<T> Come un Riferimento Possiamo riscrivere il codice nel Listato 15-6 per usare un Box<T> invece di un riferimento; l'operatore di dereferenziazione usato su Box<T> nel Listato 15-7 funziona allo stesso modo dell'operatore di dereferenziazione usato sul riferimento nel Listato 15-6: ```rust
fn main() {
let x = 5;
let y = Box::new(x);

assert_eq!(5, x);
assert_eq!(5, *y);
}

La differenza principale tra il Listato 15-7 e il Listato 15-6 è che qui abbiamo impostato y come un’istanza di un Box<T> che punta a una copia del valore di x anziché un riferimento che punta al valore di x. Nell’ultima affermazione, possiamo usare l’operatore di dereferenziazione per seguire il puntatore del Box<T> nello stesso modo in cui abbiamo fatto quando y era un riferimento. Successivamente, esploreremo cosa c’è di speciale in Box<T> che ci consente di usare l’operatore di dereferenziazione definendo il nostro tipo. Definizione del Nostro Puntatore Intelligente

Costruiamo un puntatore intelligente simile al tipo Box<T> fornito dalla libreria standard per sperimentare come i puntatori intelligenti si comportino diversamente dai riferimenti per impostazione predefinita. Poi vedremo come aggiungere la capacità di usare l’operatore di dereferenziazione.

Il tipo Box<T> è definito in ultima analisi come una tupla struct con un solo elemento, quindi il Listato 15-8 definisce un tipo MyBox<T> allo stesso modo. Definiremo anche una nuova funzione per corrispondere alla nuova funzione definita su Box<T>.

rust

struct MyBox<T>(T);

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

Abbiamo definito una struct chiamata MyBox e dichiarato un parametro generico T, perché vogliamo che il nostro tipo contenga valori di qualsiasi tipo. Il tipo MyBox è una tupla struct con un solo elemento di tipo T. La funzione MyBox::new prende un parametro di tipo T e restituisce un’istanza di MyBox che contiene il valore passato.

Proviamo ad aggiungere la funzione main nel Listato 15-7 al Listato 15-8 e cambiare per usare il tipo MyBox<T> che abbiamo definito invece di Box<T>. Il codice nel Listato 15-9 non verrà compilato perché Rust non sa come dereferenziare MyBox.

rust

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

assert_eq!(5, x);
assert_eq!(5, *y);
}

Ecco l’errore di compilazione risultante:

rust

$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
--> src/main.rs:14:19
|
14 | assert_eq!(5, *y);
| ^^
Per maggiori informazioni su questo errore, prova `rustc --explain E0614`.
errore: non è possibile compilare `deref-example` a causa di un errore precedente Il nostro tipo MyBox<T> non può essere dereferenziato perché non abbiamo implementato tale capacità sul nostro tipo. Per abilitare la dereferenziazione con l'operatore *, implementiamo il trait Deref.
Trattare un Tipo Come un Riferimento Implementando il Trait Deref Come discusso nella sezione "Implementare un Trait su un Tipo" del Capitolo 10, per implementare un trait, dobbiamo fornire implementazioni per i metodi richiesti dal trait. Il trait Deref, fornito dalla libreria standard, ci richiede di implementare un metodo chiamato deref che prende in prestito self e restituisce un riferimento ai dati interni. Il Listato 15-10 contiene un'implementazione di Deref da aggiungere alla definizione di MyBox: ```rust
use std::ops::Deref; impl<T> Deref for MyBox<T> {
type Target = T;

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

La sintassi type Target = T; definisce un tipo associato per il trait Deref da utilizzare. I tipi associati sono un modo leggermente diverso di dichiarare un parametro generico, ma non devi preoccuparti di loro per ora; li tratteremo in dettaglio nel Capitolo 19.

Compiliamo il corpo del metodo deref con &self.0 in modo che deref restituisca un riferimento al valore a cui vogliamo accedere con l’operatore *; ricorda dalla sezione “Usare Tuple Structs Senza Campi Nominati per Creare Tipi Diversi” del Capitolo 5 che .0 accede al primo valore in una tuple struct. La funzione main nel Listato 15-9 che chiama * sul valore MyBox<T> ora compila, e le asserzioni passano!

Senza il trait Deref, il compilatore può dereferenziare solo i riferimenti &. Il metodo deref dà al compilatore la possibilità di prendere un valore di qualsiasi tipo che implementa Deref e chiamare il metodo deref per ottenere un riferimento & che sa come dereferenziare.

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

*(y.deref())

Rust sostituisce l’operatore * con una chiamata al metodo deref e poi una dereferenziazione semplice in modo che non dobbiamo pensare a se dobbiamo o meno chiamare il metodo deref. Questa caratteristica di Rust ci permette di scrivere codice che funziona in modo identico se abbiamo un riferimento normale o un tipo che implementa Deref.

Il motivo per cui il metodo deref restituisce un riferimento a un valore e che la dereferenziazione semplice al di fuori delle parentesi in *(y.deref()) è ancora necessaria, riguarda il sistema di proprietà. Se il metodo deref restituisse direttamente il valore anziché un riferimento al valore, il valore sarebbe spostato da self. Non vogliamo prendere la proprietà del valore interno dentro MyBox<T> in questo caso o nella maggior parte dei casi in cui usiamo l’operatore di dereferenziazione.

Nota che l’operatore * viene sostituito con una chiamata al metodo deref e poi una chiamata all’operatore * solo una volta, ogni volta che usiamo un * nel nostro codice. Poiché la sostituzione dell’operatore * non si ricorsa all’infinito, otteniamo alla fine dati di tipo i32, che corrisponde al 5 in assert_eq! nel Listato 15-9. Coercizioni Impliciti del Deref con Funzioni e Metodi

La coercizione del deref converte un riferimento a un tipo che implementa il trait Deref in un riferimento a un altro tipo. Ad esempio, la coercizione del deref può convertire &String in &str perché String implementa il trait Deref in modo che restituisca &str. La coercizione del deref è una comodità che Rust esegue sugli argomenti delle funzioni e dei metodi, e funziona solo su tipi che implementano il trait Deref. Si verifica automaticamente quando passiamo un riferimento a un valore di un tipo particolare come argomento a una funzione o a un metodo che non corrisponde al tipo del parametro nella definizione della funzione o del metodo. Una sequenza di chiamate al metodo deref converte il tipo che abbiamo fornito nel tipo di cui ha bisogno il parametro.

La coercizione del deref è stata aggiunta a Rust in modo che i programmatori che scrivono chiamate di funzioni e metodi non debbano aggiungere tanti riferimenti espliciti e dereferenziazioni con & e *. La funzionalità di coercizione del deref ci consente anche di scrivere più codice che può funzionare sia per i riferimenti che per i puntatori intelligenti.

Per vedere la coercizione del deref in azione, usiamo il tipo MyBox<T> che abbiamo definito nel Listato 15-8 così come l’implementazione di Deref che abbiamo aggiunto nel Listato 15-10. Il Listato 15-11 mostra la definizione di una funzione che ha un parametro slice di stringhe:

rust

fn hello(name: &str) {
println!("Ciao, {name}!");
}

Possiamo chiamare la funzione hello con un slice di stringhe come argomento, ad esempio hello(“Rust”); per esempio. La coercizione del deref rende possibile chiamare hello con un riferimento a un valore di tipo MyBox<String>, come mostrato nel Listato 15-12:

rust

fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}

Qui stiamo chiamando la funzione hello con l’argomento &m, che è un riferimento a un valore MyBox<String>. Poiché abbiamo implementato il trait Deref su MyBox<T> nel Listato 15-10, Rust può trasformare &MyBox<String> in &String chiamando deref. La libreria standard fornisce un’implementazione di Deref su String che restituisce un slice di stringhe, e questo è presente nella documentazione API di Deref. Rust chiama deref di nuovo per trasformare &String in &str, che corrisponde alla definizione della funzione hello.

Se Rust non implementasse la coercizione del deref, dovremmo scrivere il codice nel Listato 15-13 invece del codice nel Listato 15-12 per chiamare hello con un valore di tipo &MyBox<String>.

rust

fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}

Il (*m) dereferenzia il MyBox<String> in una String. Quindi il & e il [..] prendono un slice di stringhe della String che è uguale all’intera stringa per corrispondere alla firma di hello. Questo codice senza coercioni del deref è più difficile da leggere, scrivere e capire con tutti questi simboli coinvolti. La coercizione del deref consente a Rust di gestire queste conversioni automaticamente per noi.

Quando il trait Deref è definito per i tipi coinvolti, Rust analizzerà i tipi e userà Deref::deref quante volte necessario per ottenere un riferimento che corrisponda al tipo del parametro. Il numero di volte che Deref::deref deve essere inserito viene risolto a tempo di compilazione, quindi non c’è alcuna penalità durante l’esecuzione per sfruttare la coercizione del deref! Come la Coercizione del Deref Interagisce con la Mutabilità

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

Rust esegue la coercizione del deref quando trova tipi e implementazioni di trait in tre casi:

r

Da &T a &U quando T: Deref<Target=U>
Da &mut T a &mut U quando T: DerefMut<Target=U>
Da &mut T a &U quando T: Deref<Target=U>

I primi due casi sono gli stessi tra loro tranne che il secondo implementa la mutabilità. Il primo caso afferma che se hai un &T, e T implementa Deref per un qualche tipo U, puoi ottenere un &U in modo trasparente. Il secondo caso afferma che la stessa coercizione del deref avviene per i riferimenti mutabili.

Il terzo caso è più complicato: Rust coercerà anche un riferimento mutabile in uno immutabile. Ma il contrario non è possibile: i riferimenti immutabili non si coerceranno mai in riferimenti mutabili. A causa delle regole di prestito, se hai un riferimento mutabile, quel riferimento mutabile deve essere l’unico riferimento a quei dati (altrimenti, il programma non verrebbe compilato). Convertire un riferimento mutabile in uno immutabile non violerebbe mai le regole di prestito. Convertire un riferimento immutabile in uno mutabile richiederebbe che il riferimento immutabile iniziale sia l’unico riferimento immutabile a quei dati, ma le regole di prestito non garantiscono ciò. Pertanto, Rust non può fare l’assunzione che la conversione di un riferimento immutabile in uno mutabile sia possibile.

1 Comment

Leave a Reply

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *