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.