Deref y DerefMut
Deref
es un rasgo que permite utilizar *
para desreferenciar una variable. Deref
ha aparecido anteriormente cuando se usaba una estructura de tupla para crear un nuevo tipo. Ahora es el momento de aprender su uso.
Se conoce ya que una referencia no es lo mismo que un valor:
// ⚠️ fn main() { let valor = 7; // Esto es un i32 let referencia = &7; // Esto es un &i32 println!("{}", valor == referencia); }
En este caso, Rust no devuelve false
en la comparación. Da error de compilación:
error[E0277]: can't compare `{integer}` with `&{integer}`
--> src\main.rs:4:26
|
4 | println!("{}", valor == referencia);
| ^^ no implementation for `{integer} == &{integer}`
La solución en este caso es el uso de *
. Que dará como resultado true
.
fn main() { let valor = 7; let referencia = &7; println!("{}", valor == *referencia); }
A continuación, se construye un tipo simple que solamente contiene un número. Este tipo será parecido al tipo Box
. Si solamente se crea el tipo con el número, no se podrá hacer mucho con él.
No se puede usar *
, como sí se puede con Box
:
// ⚠️ struct GuardaUnNumero(u8); fn main() { let mi_numero = GuardaUnNumero(20); println!("{}", *mi_numero + 20); }
El error dice:
error[E0614]: type `GuardaUnNumero` cannot be dereferenced
--> src/main.rs:6:20
|
6 | println!("{}", *mi_numero + 20);
| ^^^^^^^^^^
Sí se podría hacer algo como esto println!("{:?}", mi_numero.0 + 20);
, pero entonces solo se están sumando un u8
a 20
. Lo que sería práctico es sumar la variable con el valor. El mensaje cannot be dereferenced
da una pista: es necesario implementar Deref
. A los elementos simples que implementan este rasgo se los suele llamar punteros inteligentes- Un punterio inteligente puede apuntar a su elemento, tiene información adicional sobre él y puede tener y usar diversos métodos. El que se está construyendo puede hacer poca cosa en este momento, sumar un número e imprimirlo en println!
ya que implementa Debug
.
Cabe destacar un hecho interesante: String
es un puntero inteligente a &str
y Vec
es un puntero inteligente a un array u otro tipo. Desde el comienzo, se han estado presentando punteros inteligentes.
La implementación de Deref
no es difícil y los ejemplos de la librería estándar son fáciles. (Este es un código de ejemplo de la librería estándar)[https://doc.rust-lang.org/std/ops/trait.Deref.html]:
use std::ops::Deref; struct DerefExample<T> { value: T } impl<T> Deref for DerefExample<T> { type Target = T; fn deref(&self) -> &Self::Target { &self.value } } fn main() { let x = DerefExample { value: 'a' }; assert_eq!('a', *x); }
Siguiendo el ejemplo, el tipo que se está creando necesita:
#![allow(unused)] fn main() { // 🚧 impl Deref for GuardaUnNumero { type Target = u8; // Recuerda, este es el "tipo asociado": el tipo al que va unido. // Hay que poner el tipo correcot de retorno para Target fn deref(&self) -> &Self::Target { // Rust llama a .deref() cuando se usa *. solo se ha definido el Target como u8 &self.0 // Se elige &self.0 porque estamos en un struct tupla. En una struct con campos con nombres se usaría "&self.numero" } } }
Ahora sí se puede usar *
:
use std::ops::Deref; #[derive(Debug)] struct GuardaUnNumero(u8); impl Deref for GuardaUnNumero { type Target = u8; fn deref(&self) -> &Self::Target { &self.0 } } fn main() { let mi_numero = GuardaUnNumero(20); println!("{:?}", *mi_numero + 20); }
El código anterior imprime 40
sin que haya habido necesidad de referirse al valor almacenado en el struct
con mi_numero.0
. Así se dispone de los métodos de u8
y se pueden añadir otros métodos a GuardaUnNumero
. A continuación, se observa esto mediante el uso de un método denominado .checked_sub()
. Se trata de una resta segura que devuelve un Option
. Si puede hacer la resta, devuelve un Some
con el resultado en su interior. Si no puede hacerla, devuelve None
. Se debe recordar que u8
no puede guardar números negativos. Por ello, es más seguro usar .checked_sub()
evitando que el programa entre en pánico.
use std::ops::Deref; struct GuardaUnNumero(u8); impl GuardaUnNumero { fn imprime_el_doble_del_numero(&self) { println!("{}", self.0 * 2); } } impl Deref for GuardaUnNumero { type Target = u8; fn deref(&self) -> &Self::Target { &self.0 } } fn main() { let mi_numero = GuardaUnNumero(20); println!("{:?}", mi_numero.checked_sub(100)); // Este método viene de u8 mi_numero.imprime_el_doble_del_numero(); // Este viene del struct GuardaUnNumero }
Este código imprime:
None
40
DerefMut
También se puede implementar DerefMut
que permite modificar los valores a través de *
. Es muy parecido. Es necesario haber implementado Deref
para poder implementar DerefMut
.
use std::ops::{Deref, DerefMut}; struct GuardaUnNumero(u8); impl GuardaUnNumero { fn imprime_el_doble_del_numero(&self) { println!("{}", self.0 * 2); } } impl Deref for GuardaUnNumero { type Target = u8; fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for GuardaUnNumero { // Aquí no se necesita Target = u8; ya que se conoce debido a Deref fn deref_mut(&mut self) -> &mut Self::Target { // es igual que en Deref, pero con mut &mut self.0 } } fn main() { let mut mi_numero = GuardaUnNumero(20); *mi_numero = 30; // DerefMut permite que se pueda hacer esto println!("{:?}", mi_numero.checked_sub(100)); mi_numero.imprime_el_doble_del_numero(); }
Esto imprime:
#![allow(unused)] fn main() { None 60 }
Se observa que Deref
permite muchas posibilidades.
Por ello, la librería estándar dice que Con el fin de evitar confusiones, Deref solo debería implementarse para punteros inteligentes. Esto se debe a que se pueden hacer cosas extrañas con Deref
si un tipo es compuesto. A continuación se muestra un ejemplo desconcertante. Se crea un struct Personaje
para un juego. Este personaje tiene diversos atributos.
struct Personaje { nombre: String, fuerza: u8, destreza: u8, salud: u8, inteligencia: u8, sabiduria: u8, encanto: u8, puntos_de_golpeo: i8, alineamiento: Alineamiento, } impl Personaje { fn new( nombre: String, fuerza: u8, destreza: u8, salud: u8, inteligencia: u8, sabiduria: u8, encanto: u8, puntos_de_golpeo: i8, alineamiento: Alineamiento, ) -> Self { Self { nombre, fuerza, destreza, salud, inteligencia, sabiduria, encanto, puntos_de_golpeo, alineamiento, } } } enum Alineamiento { Bueno, Neutral, Malvado, } fn main() { let billy = Personaje::new("Billy".to_string(), 9, 8, 7, 10, 19, 19, 5, Alineamiento::Bueno); }
A continuación, se modifica para mantener los puntos de golpeo en un vector. Puede que se incorpore en este vector los datos de los monstruos, para mantenerlos juntos. Puesto que puntos_de_golpeo
es un i8
, se implementa Deref
para poder hacer toda clase de operaciones matemáticas con él. Pero queda muy extraño su uso en main()
:
use std::ops::Deref; // todo el código es igual hasta el enum Alineamiento struct Personaje { nombre: String, fuerza: u8, destreza: u8, salud: u8, inteligencia: u8, sabiduria: u8, encanto: u8, puntos_de_golpeo: i8, alineamiento: Alineamiento, } impl Personaje { fn new( nombre: String, fuerza: u8, destreza: u8, salud: u8, inteligencia: u8, sabiduria: u8, encanto: u8, puntos_de_golpeo: i8, alineamiento: Alineamiento, ) -> Self { Self { nombre, fuerza, destreza, salud, inteligencia, sabiduria, encanto, puntos_de_golpeo, alineamiento, } } } enum Alineamiento { Bueno, Neutral, Malvado, } impl Deref for Personaje { // impl Deref en Personaje. Ahora hace cualquier cómputo integer sobre los puntos de golpeo sin mostralo! type Target = i8; fn deref(&self) -> &Self::Target { &self.puntos_de_golpeo } } fn main() { let billy = Personaje::new("Billy".to_string(), 9, 8, 7, 10, 19, 19, 5, Alineamiento::Bueno); let brandy = Personaje::new("Brandy".to_string(), 9, 8, 7, 10, 19, 19, 5, Alineamiento::Bueno); let mut vec_puntos_de_golpeo = vec![]; vec_puntos_de_golpeo.push(*billy); // ¿Push de *billy qué está guardando? vec_puntos_de_golpeo.push(*brandy); // ¿Push de *brandy qué está guardando? println!("{:?}", vec_puntos_de_golpeo); }
Esto imprime solo [5, 5]
. El código resulta extraño al leerlo. Es necesario ir a mirar que el Deref
se está haciendo sobre un atributo concreto de Personaje
. Si este conocimiento, no es posible entender qué se está guardando cuando se usa el *
.
En este caso, el lugar de implementar Deref
, resulta mejor implementar un método .get_puntos_de_golpeo()
. Deref
ofrece mucha potencia, pero es necesario usarlo solo donde resulte lógico hacerlo.