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.
fn main() { let x = 5; let y = &x; assert_eq!(5, x); assert_eq!(5, *y); }
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.
fn main() { let x = 5; let y = Box::new(x); assert_eq!(5, x); assert_eq!(5, *y); }
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 veroBox<T>
: la nostra versione non memorizzerà i dati nell’heap. Ci stiamo concentrando suDeref
, 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>
.
struct MioBox<T>(T); impl<T> MioBox<T> { fn new(x: T) -> MioBox<T> { MioBox(x) } } fn main() {}
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
.
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);
}
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>
.
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); }
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.
fn ciao(nome: &str) { println!("Ciao, {nome}!"); } fn main() {}
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.
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); }
ciao
con un reference a un valore MioBox<String>
, che funziona grazie alla deref coercionQui 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>
.
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)[..]); }
(*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:
- Da
&T
a&U
quandoT: Deref<Target=U>
- Da
&mut T
a&mut U
quandoT: DerefMut<Target=U>
- Da
&mut T
a&U
quandoT: 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.