rust-smart-pointers-deref-drop

Deref y Drop en Rust: base de smart pointers seguros

  • 4 min

Deref y Drop son traits para desreferenciar y limpiar recursos en tipos como los punteros inteligentes.

Un Smart Pointer suele ser un Struct.

  • Box<T> es un struct.
  • String es un struct.
  • Rc<T> es un struct.

Normalmente, no puedes desreferenciar un struct (*mi_struct daría error). Todos los valores se destruyen al salir de su ámbito y sus campos también se destruyen; para añadir una acción de limpieza propia implementamos Drop.

El trait Deref

El trait Deref permite personalizar el comportamiento del operador de desreferencia *.

Sin Deref, si creamos nuestro propio puntero inteligente, tendríamos que acceder al valor interno manualmente cada vez.

Implementando nuestro propio Box

Vamos a crear un struct tonto llamado MiCaja para ver el problema.

struct MiCaja<T>(T); // Tuple struct genérico

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

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

    assert_eq!(5, x);
    // assert_eq!(5, *y); // ❌ Error: type `MiCaja<{integer}>` cannot be dereferenced
}
Copied!

Rust no sabe cómo aplicar * a MiCaja. Para arreglarlo, implementamos Deref:

use std::ops::Deref;

impl<T> Deref for MiCaja<T> {
    type Target = T; // Definimos qué tipo hay dentro

    fn deref(&self) -> &Self::Target {
        &self.0 // Devolvemos una referencia al primer elemento de la tupla
    }
}
Copied!

Ahora, cuando escribimos *y, Rust en realidad ejecuta tras bambalinas: *(y.deref())

Obtiene la referencia interna y luego la desreferencia. ¡Ahora nuestro struct se comporta como un puntero!

Coerción de Deref

Gracias a Deref, Rust ofrece una característica de calidad de vida increíble llamada Deref Coercion.

Rust puede convertir automáticamente una referencia a un puntero inteligente en una referencia a su contenido. Y lo hace en cadena.

Imagina una función que espera un slice de string (&str):

fn saludar(nombre: &str) {
    println!("Hola, {}!", nombre);
}

fn main() {
    let m = MiCaja::new(String::from("Rust"));

    // Llamamos a la función pasando &MiCaja<String>
    saludar(&m); // ✅ ¡Funciona!
}
Copied!

¿Qué ha pasado aquí?

  1. Rust ve que saludar quiere &str.
  2. Le pasamos &MiCaja. Llama a deref -> obtenemos &String.
  3. String también implementa Deref (hacia str). Llama a deref -> obtenemos &str.
  4. ¡Coincide!

Sin esta característica, tendríamos que haber escrito: saludar(&(*m)[..]). Gracias Deref.

El trait Drop

El segundo pilar es Drop. Este trait nos permite ejecutar código cuando un valor va a salir del ámbito (scope). Es el equivalente a un Destructor en C++.

Es importante para los Smart Pointers:

  • Box usa Drop para liberar la memoria del Heap.
  • Rc usa Drop para restar 1 al contador de referencias.
  • File usa Drop para cerrar el archivo.
  • MutexGuard usa Drop para liberar el bloqueo.

Implementando Drop

Vamos a crear un struct ruidoso que nos avise cuando muere.

struct PunteroRuidoso {
    dato: String,
}

impl Drop for PunteroRuidoso {
    fn drop(&mut self) {
        println!("Limpiando puntero con dato: `{}`", self.dato);
    }
}

fn main() {
    let c = PunteroRuidoso { dato: String::from("C") };
    let d = PunteroRuidoso { dato: String::from("D") };

    println!("Punteros creados.");
} // Aquí termina el scope
Copied!

Salida:

Punteros creados.
Limpiando puntero con dato: `D`
Limpiando puntero con dato: `C`
Copied!

Fíjate en el orden: las variables locales se destruyen en orden inverso a su declaración. Por eso d se destruye antes que c, independientemente de cómo decida el compilador almacenar físicamente esos valores.

Forzando la limpieza (std::mem::drop)

A veces quieres liberar algo antes de que termine el ámbito (por ejemplo, para liberar un Lock o cerrar un archivo para que otro proceso pueda usarlo).

No puedes llamar al método drop manualmente:

// c.drop(); // ❌ Error explícito: Destructor explícito no permitido
Copied!

Rust prohíbe esto para evitar el error de “Double Free” (intentar liberar memoria dos veces: una manual y otra automática al final del scope).

Si quieres forzarlo, usa la función de la librería estándar:

fn main() {
    let c = PunteroRuidoso { dato: String::from("C") };

    println!("Antes del drop");
    std::mem::drop(c); // Forzamos la liberación aquí
    println!("Después del drop");
}
Copied!