Drop y la liberación determinista
Cuándo exactamente se libera un valor, en qué orden, y por qué el trait `Drop` da RAII —liberación garantizada de memoria, ficheros y locks— sin destructores explícitos ni recolector de basura.
La tercera regla de ownership dice “al salir de scope, el valor se libera”. El trait Drop es esa regla hecha código: el gancho que Rust ejecuta en el instante exacto de la muerte de un valor. De él sale el RAII —liberación determinista de cualquier recurso, no solo memoria— sin que tú escribas un destructor, sin recolector, y con un momento de ejecución que puedes leer directamente en las llaves.
- Saber el momento exacto y el orden en que se ejecutan los
drop. - Implementar el trait
Dropy ver RAII en acción sobre un recurso real. - Liberar antes de tiempo con
drop()y entender por qué no puedes llamar a.drop(). - Conocer los límites: cuándo un destructor puede no ejecutarse.
El momento y el orden
Un valor se libera al final del bloque que lo posee, salvo que se haya movido antes. Dentro de un scope, las variables locales se liberan en orden inverso a su declaración (una pila: lo último en nacer es lo primero en morir). Los campos de un struct y los elementos de un Vec, en cambio, se liberan en orden de declaración.
struct Ruidoso(&'static str);
impl Drop for Ruidoso {
fn drop(&mut self) {
println!("libero {}", self.0);
}
}
fn main() {
let _a = Ruidoso("a");
let _b = Ruidoso("b");
let _c = Ruidoso("c");
} // imprime: libero c, libero b, libero a
flowchart TB crea[Se declaran a luego b luego c] --> fin[Fin del scope] fin --> dc[drop c] dc --> db[drop b] db --> da[drop a]
Para valores compuestos el orden interno se invierte respecto a las variables locales: primero corre el Drop::drop del contenedor (si lo implementa) y luego se liberan sus campos, esta vez en orden de declaración.
struct Contenedor {
primero: Ruidoso,
segundo: Ruidoso,
}
fn main() {
let _c = Contenedor {
primero: Ruidoso("primero"),
segundo: Ruidoso("segundo"),
};
} // imprime: libero primero, libero segundo (campos en orden de declaración)
El orden LIFO de las variables locales no es capricho: si b se construyó tomando prestado algo de a, b tiene que morir antes que a para que su destructor no toque un a ya liberado. Liberar en orden inverso al de creación respeta esas dependencias por construcción: lo último que nació es lo primero que muere, justo cuando ya nada depende de ello.
RAII: el recurso se libera solo
Lo poderoso es que este mecanismo no sabe nada de memoria: sirve para cualquier recurso. Un fichero se cierra en su drop, una conexión se termina en su drop, un lock se suelta en su drop. Adquieres el recurso al construir el valor y lo sueltas al destruirlo: eso es RAII (Resource Acquisition Is Initialization), y en Rust es automático y determinista.
use std::sync::Mutex;
fn main() {
let m = Mutex::new(0);
{
let mut guard = m.lock().unwrap(); // adquiere el lock
*guard += 1;
} // el guard sale de scope aquí: el lock se suelta, garantizado
println!("{:?}", m.lock().unwrap());
}
No hay unlock() que puedas olvidar, ni un finally que escribir. El MutexGuard posee el lock; cuando muere, lo suelta. Compáralo con un GC, donde el finalizador corre “en algún momento” indeterminado, o con C, donde soltar el recurso es tu responsabilidad y tu bug.
Como el lock se suelta al final del scope del guard, puedes controlar cuánto dura la sección crítica con un simple bloque { }: mete dentro solo lo que necesite el lock y ciérralo cuanto antes. El mismo patrón sirve para cerrar un fichero pronto o liberar un buffer temporal. En Rust, “acotar la vida de un recurso” y “acotar un scope” son la misma acción.
Liberar antes de tiempo
A veces quieres liberar antes del final del scope —soltar un lock cuanto antes, o liberar un buffer enorme—. Se hace con la función libre drop, que no es el método del trait:
fn main() {
let pesado = String::from("un buffer grande");
drop(pesado); // se libera YA, no al final del scope
// println!("{pesado}"); // ERROR: pesado fue movido a drop
}
El truco es elegante: std::mem::drop está definido, literalmente, como fn drop<T>(_x: T) {}. No hace nada; se limita a tomar la propiedad del valor, que muere al terminar esa diminuta función. Por eso funciona con cualquier tipo. Y por eso el compilador te prohíbe (error E0040) llamar a valor.drop() directamente: eso ejecutaría el destructor pero dejaría el valor “vivo” para un segundo drop automático al final del scope, justo el doble free que todo el sistema evita.
Drop en todos los caminos de salida
La garantía de la vía normal es más fuerte de lo que parece: ningún camino de salida de una función se salta el drop. Un return temprano, un operador ? que propaga un error, o un panic que desenrolla la pila —todos ejecutan los destructores de lo que haya vivo en ese scope antes de irse.
fn procesa(fallar: bool) -> Result<(), String> {
let _recurso = Ruidoso("recurso");
if fallar {
return Err("fallo temprano".into()); // `_recurso` se libera aquí igual
}
trabajo()?; // si `?` propaga el error, `_recurso` se libera al salir
Ok(())
}
Esto es lo que hace del RAII de Rust algo fiable y no un buen deseo: no existe la ruta de código olvidada que en C deja un free sin ejecutar tras un goto fail. Con panic = "unwind" (el modo por defecto), incluso un pánico desenrolla la pila corriendo los drop; solo panic = "abort" los omite, porque el proceso muere de golpe.
Los límites de la garantía
Aquí un matiz de nivel PhD que casi nadie explica bien: la ejecución de drop no está garantizada por seguridad de memoria, solo por la vía normal. Un valor puede terminar su vida sin que su destructor corra: si lo pasas a std::mem::forget, si lo envuelves en ManuallyDrop, si forma parte de un ciclo de Rc/Arc, o si el proceso llama a std::process::exit. Crucialmente, filtrar memoria es seguro en Rust (no es memory-unsafe): desperdicia recursos, pero no corrompe nada, y por eso el lenguaje lo permite.
use std::mem;
fn main() {
let s = String::from("se filtra a propósito");
mem::forget(s); // el destructor NO corre: el buffer queda sin liberar
} // no imprime nada de drop; es un leak, pero un leak seguro
Que forget sea una función segura (sin unsafe) es una decisión de diseño profunda: Rust garantiza que no habrá use-after-free ni doble free, pero no garantiza que todo destructor corra. Seguridad de memoria y ausencia de fugas son propiedades distintas, y solo la primera es innegociable.
Además de forget, la biblioteca ofrece ManuallyDrop<T> para suprimir el destructor de forma estructurada, y Box::leak para convertir una asignación en una referencia 'static filtrada a propósito. Son herramientas de nicho —FFI, arenas de memoria, valores estáticos—, pero confirman la filosofía: en Rust, no liberar es una elección explícita y segura, jamás un accidente silencioso.
Dos precisiones más. Primera: cuando el compilador no puede saber estáticamente si un valor fue movido o no antes del fin del scope, inserta un drop flag, un booleano en tiempo de ejecución que decide si toca liberar. Segunda, y fácil de confundir: el drop es léxico —ocurre en la llave de cierre—, mientras que el borrow checker moderno (NLL) da por terminados los préstamos en su último uso. Un valor puede tener su préstamo cerrado mucho antes de que su drop se ejecute; son dos relojes distintos.
Como Drop puede no ejecutarse (forget, ciclos de Rc, abort en pánico), no lo conviertas en la única garantía de algo que debe ocurrir sí o sí —confirmar una transacción, un flush imprescindible, un mensaje de auditoría—. Para liberar recursos es perfecto; para lógica cuya omisión sería un bug de correctitud, ten además un camino explícito que no dependa del destructor.
Casi toda la atención va al borrow checker, pero el RAII de Drop es, en la práctica diaria, tan transformador como el ownership que lo sustenta. En un lenguaje con GC sabes que la memoria se liberará, pero no cuándo, y para recursos que no son memoria —locks, ficheros, conexiones, transacciones— el GC no te sirve: acabas escribiendo try/finally, defer o with, recordando a mano cada liberación. Rust colapsa todos esos casos en una sola idea: el recurso vive exactamente tanto como su dueño, y su liberación está escrita en la estructura léxica del programa. No hay ruta de código, por enrevesada que sea —un return temprano, un ? que propaga un error, un panic que desenrolla la pila—, que pueda saltarse el drop en la vía normal. Esa es la razón por la que un MutexGuard de Rust es imposible de dejar sin soltar por accidente, y por la que el patrón “adquiere y olvídate” es correcto por construcción. El ownership decide quién libera; Drop decide, con precisión de reloj, cuándo.
Un valor se libera al final de su scope léxico, en orden inverso al de declaración, ejecutando su Drop::drop si lo tiene. Ese momento es determinista y visible en las llaves del código. De ahí sale el RAII: memoria, locks, ficheros y conexiones se sueltan solos y a tiempo, sin GC y sin free manual. La única letra pequeña es que, por diseño, no todo destructor está garantizado a correr.
- Implementa
Droppara un tipo con un campo&'static stry crea tres instancias en un scope; predice el orden de las líneas antes de ejecutar. - Añade un
structque contenga dos de esos tipos como campos y comprueba que los campos se liberan en orden de declaración, no inverso. - Usa
drop(x)para liberar un valor a mitad de una función y confirma que ya no puedes usarlo después. - Investiga
std::mem::forgety explica en una frase por qué filtrar memoria puede ser seguro aunqueDropno corra. - Añade un
returntemprano dentro de una función con un valorDropvivo y confirma, por la salida, que el destructor corre igualmente.