wandres.dev
DEREF, DROP Y OPERADORES · traits que dan superpoderes

Drop: el destructor determinista y por qué no puedes llamarlo

`Drop` es el único trait que el compilador invoca por ti, en el instante de la muerte de un valor. Diseccionamos su mecanismo: por qué su método recibe `&mut self` y no `self`, qué es el drop glue que libera los campos, el orden exacto de destrucción, la función libre `std::mem::drop`, y por qué llamar a `.drop()` a mano es el error E0040.

⏱ 18 min

De todos los traits, Drop es el único que tú nunca llamas: lo invoca el compilador, en un momento que no escribes en ninguna parte, el de la muerte de un valor. Es el destructor de Rust, y su ejecución es determinista —ocurre en la llave de cierre del scope, no “en algún momento” como el finalizador de un recolector—. En el nivel 8 lo viste como la tercera regla de ownership y fuente del RAII; aquí lo abrimos por dentro como el trait especial que es: por qué su método recibe &mut self, qué hace el drop glue, en qué orden cae todo, y por qué llamar a .drop() con la mano es un error de compilación.

🎯 Al terminar esta lección sabrás
  • Implementar Drop y saber el instante exacto en que corre su método.
  • Entender por qué drop recibe &mut self y qué es el drop glue que libera los campos.
  • Liberar antes de tiempo con std::mem::drop y ver que es fn drop<T>(_x: T) {}.
  • Explicar el error E0040 al intentar llamar a .drop() directamente.

El único trait que llama el compilador

Drop tiene un solo método, y su firma esconde la primera sorpresa: recibe &mut self, no self.

pub trait Drop {
    fn drop(&mut self);   // toma prestado, no consume
}

Se ejecuta cuando el valor muere: al final de su scope, si no se movió antes. Las variables locales caen en orden inverso al de declaración —una pila: lo último en nacer es lo primero en morir—; los campos de un struct, en orden de declaración.

struct Ruidoso(&'static str);

impl Drop for Ruidoso {
    fn drop(&mut self) { println!("cae {}", self.0); }
}

fn main() {
    let _a = Ruidoso("a");
    let _b = Ruidoso("b");
} // imprime: cae b, cae a

Por qué drop recibe mut self, y el drop glue

¿Por qué &mut self y no self? Porque si drop consumiera el valor por valor, al terminar su cuerpo ese self tendría que destruirse… llamando a drop, que volvería a recibirlo, y así hasta el infinito. Tomándolo prestado, tu método hace su limpieza con acceso mutable, y cuando termina, el compilador ejecuta el drop glue: el código generado que libera recursivamente cada campo.

struct Recurso { nombre: String }   // el campo String tiene su propio destructor

impl Drop for Recurso {
    fn drop(&mut self) {
        println!("cierro {}", self.nombre);   // 1) tu limpieza, con &mut self
    }
    // 2) al salir, el drop glue libera el campo `nombre` (su String)
}

Esta división es la clave: te encargas de tu lógica de cierre; el glue se encarga de los miembros, y no puedes impedírselo. La destrucción es por eso total y recursiva por construcción: cada valor que muere arrastra a todos los que posee.

flowchart TB
F[Fin del scope o llamada a drop] --> Y[Corre tu metodo drop con mut self]
Y --> G[Drop glue del compilador]
G --> C1[Libera el primer campo]
G --> C2[Libera el segundo campo]
C1 --> Z[Valor destruido exactamente una vez]
C2 --> Z
style Y fill:#89b4fa,color:#11111b
style G fill:#f9e2af,color:#11111b
style Z fill:#a6e3a1,color:#11111b

Liberar antes de tiempo: std::mem::drop

A veces quieres que un valor muera antes del final del scope —soltar un lock cuanto antes, liberar un buffer enorme—. Para eso está la función libre std::mem::drop, que no es el método del trait. Su definición es casi un chiste:

// En la biblioteca estandar, literalmente:
pub fn drop<T>(_x: T) {}   // toma posesion; _x muere al acabar la funcion

fn main() {
    let pesado = String::from("un buffer grande");
    drop(pesado);            // se libera YA, no al final del scope
    // pesado ya no es usable: se movio a drop
}

No hace nada en su cuerpo: se limita a tomar la propiedad del valor por valor, que entonces muere al terminar esa diminuta función —momento en el que corren su destructor y su glue—. Por eso funciona con cualquier T, tenga Drop o no.

Por qué .drop() es el error E0040

Si el método existe, ¿por qué no puedo llamar a valor.drop() yo mismo? Porque ejecutaría el destructor ahora, pero el valor seguiría vivo para el sistema de ownership, que volvería a destruirlo al final del scope: un doble destructor, exactamente el doble free que Rust erradica. Para cerrar esa grieta, el compilador prohíbe la llamada explícita con el error E0040:

fn main() {
    let r = Recurso { nombre: String::from("x") };
    // r.drop();   // ERROR E0040: explicit use of destructor method
    drop(r);       // correcto: la funcion libre mueve r y lo destruye una vez
}

La diferencia es sutil pero total: la función libre mueve el valor hacia dentro, de modo que el ownership sabe que ya no vive aquí y no lo destruye por segunda vez; el método .drop() no movería nada, dejando el valor vivo y condenado a un segundo drop. La invariante que el lenguaje defiende es exactamente una vez.

ℹ️
Copy y Drop se excluyen: un tipo con destructor no es trivialmente copiable

Un tipo no puede ser a la vez Copy y tener Drop, y ahora lo ves con precisión. Copy promete que duplicar el valor es un simple memcpy sin consecuencias; Drop promete que morir ejecuta lógica de limpieza. Si un tipo Copy tuviera destructor, cada copia de bits fabricaría un valor más con su propia limpieza pendiente, y esa limpieza —cerrar el mismo fichero dos veces, liberar el mismo buffer— correría de más. El compilador rechaza de plano cualquier tipo que intente ambas cosas: tener un destructor es, por definición, no ser trivial.

El compilador es el único que sabe destruir, y custodia la invariante de exactamente una vez

La pieza que hay que ver es que Drop es el punto donde el ownership deja de ser una regla estática sobre quién posee qué y se convierte en una garantía temporal: no solo hay un dueño, sino que su recurso se libera en un instante preciso y una sola vez. Y esa unicidad no es un buen deseo, sino una invariante que el compilador protege con tres decisiones que encajan como un mecanismo de relojería. Primera: drop recibe &mut self y no self, porque consumir el valor dentro de su propio destructor sería una recursión infinita; al prestarlo, deja que el drop glue remate liberando los campos, y así la destrucción es recursiva y total sin que tú escribas el descenso. Segunda: el glue es inevitable, puedes añadir limpieza propia pero no puedes evitar que los miembros caigan, de modo que no existe el olvido de liberar un campo. Tercera: la única forma de adelantar la muerte es la función libre std::mem::drop, que no ejecuta el destructor por ti sino que mueve el valor fuera de tu alcance para que el destructor automático corra sobre él una vez y solo una; por eso .drop() a mano está prohibido con E0040, porque duplicaría lo que debe ser único. Vistas juntas, estas reglas dicen una sola cosa: en Rust, quién libera lo decide el ownership, cuándo lo decide el scope, y cuántas veces —exactamente una— lo decide una maquinaria que no puedes ni saltarte ni disparar dos veces. El determinismo sin recolector no sale de una convención de disciplina, sino de que el compilador es el único con permiso para destruir.

📝
Lo esencial de Drop

Drop es el destructor que el compilador llama en la muerte del valor: fin del scope, orden inverso para locales, orden de declaración para campos. Su método recibe &mut self para no autoconsumirse, y al terminar el drop glue libera recursivamente los campos, sin que puedas impedirlo. Para adelantar la liberación existe la función libre std::mem::drop, definida como fn drop<T>(_x: T) {}, que mueve y destruye una vez. Llamar a .drop() a mano es el error E0040, porque provocaría un segundo destructor: la invariante es exactamente una vez.

⚔️ Disecciona el destructor
  1. Implementa Drop para Ruidoso(&'static str), crea tres instancias en un scope y predice el orden de las líneas antes de ejecutar.
  2. Explica en una frase por qué drop recibe &mut self y qué pasaría si recibiera self.
  3. Crea un struct con dos campos que impriman al morir y observa el orden en que el drop glue los libera. Compáralo con el orden de las variables locales.
  4. Usa std::mem::drop para liberar un String a mitad de main y confirma que ya no puedes usarlo. Explica qué mueve la función.
  5. Intenta escribir r.drop() y lee el error E0040. Explica qué doble ejecución está evitando el compilador.