wandres.dev
POR QUÉ RUST · seguridad sin GC, la mentalidad

La promesa de Rust

Seguridad de memoria garantizada en tiempo de compilación, sin recolector de basura y sin coste en ejecución. Qué promete Rust exactamente y cómo lo cumple con ownership y el borrow checker.

⏱ 14 min

La promesa de Rust cabe en una frase que, hasta hace poco, habría sonado a magia: seguridad de memoria demostrada por el compilador, sin recolector de basura y sin ningún coste en tiempo de ejecución. No es un eslogan de marketing: es, casi literalmente, un teorema que el compilador verifica en cada compilación. Esta lección desarma esa frase palabra por palabra y muestra el mecanismo de alto nivel que la hace cierta.

🎯 Al terminar esta lección sabrás
  • Desglosar la promesa: en compilación, sin GC, coste cero.
  • Entender ownership: cada valor tiene un único dueño.
  • Entender borrowing: prestar acceso sin ceder la propiedad.
  • Ver por qué la garantía no deja rastro en el binario.

La promesa, palabra por palabra

Tres afirmaciones que casi ningún otro lenguaje puede sostener a la vez:

  • En compilación: los errores de memoria no se detectan en ejecución con comprobaciones ni con un recolector; se rechazan antes de generar el ejecutable. Un use-after-free no es un crash: es un programa que no compila.
  • Sin GC: no hay hilo recolector, ni pausas, ni sobrecoste de memoria del heap gestionado. La liberación ocurre en puntos deterministas que el compilador conoce.
  • Coste cero (zero-cost): las abstracciones que garantizan la seguridad desaparecen en el binario. Lo que corre en la CPU es tan ajustado como el C equivalente, a veces más.

¿Cómo se sostiene todo esto sobre una sola idea? Con ownership y su guardián, el borrow checker.

flowchart TD
A[Tu codigo: dueños y prestamos] --> B[Borrow checker en compilacion]
B -->|reglas cumplidas| C[Binario nativo sin comprobaciones extra]
B -->|reglas rotas| D[Error de compilacion: el bug no llega a nacer]
style C fill:#a6e3a1,color:#11111b
style D fill:#f38ba8,color:#11111b

Ownership: cada valor tiene un dueño

La regla fundacional de Rust es de una simplicidad brutal: cada valor tiene exactamente un dueño, y cuando el dueño sale de ámbito, el valor se libera. Asignar o pasar un valor no lo copia: lo mueve, transfiriendo la propiedad.

fn main() {
    let libro = String::from("El Quijote"); // 'libro' es dueño de esta String
    let copia = libro;                        // la propiedad se MUEVE a 'copia'
    // println!("{libro}");                   // error[E0382]: value borrowed here after move
    println!("{copia}");                      // 'copia' es ahora el unico dueño
} // aqui, y solo aqui, se libera la String: una vez, de forma determinista

Al mover libro a copia, Rust invalida libro en tiempo de compilación. No puede haber dos dueños, así que no puede haber una doble liberación ni un puntero colgante: son imposibles por construcción. Y fíjate en la llave de cierre: ahí el compilador sabe que copia muere, e inserta la liberación. Ese es el Drop de Rust, el equivalente al destructor de C++ (RAII), pero exigido por el sistema de tipos.

ℹ️
Move por defecto, copia solo si la pides

Que el movimiento sea el comportamiento por defecto es una decisión de diseño profunda. Los tipos que viven en el heap (String, Vec<T>, Box<T>) se mueven para evitar copias caras accidentales. Los tipos pequeños y planos (i32, bool, char) implementan el trait Copy y se duplican bit a bit, porque copiarlos es trivial. Tú eliges: .clone() cuando quieras una copia profunda explícita, un préstamo cuando solo necesites mirar.

Borrowing: prestar sin ceder

Mover la propiedad en cada uso sería insufrible. Por eso Rust permite prestar un valor: acceder a él sin apropiárselo, mediante referencias. La regla de oro, el corazón entero del lenguaje: en cualquier instante, un dato puede tener o bien cualquier número de lectores (&T), o bien un único escritor (&mut T), pero nunca ambos a la vez.

fn longitud(s: &String) -> usize { // presta para leer: no se apropia
    s.len()
}

fn main() {
    let mut saludo = String::from("hola");
    let n = longitud(&saludo);   // prestamo inmutable: puede haber muchos
    saludo.push_str(", mundo");  // prestamo mutable exclusivo: uno y solo uno
    println!("{saludo} medía {n} al prestarla");
}

Esta regla, “aliasing XOR mutabilidad”, elimina de un plumazo las carreras de datos: una carrera necesita, por definición, dos accesos simultáneos con al menos una escritura. Si el compilador prohíbe leer y escribir a la vez, la carrera no puede existir. Por eso Rust habla de fearless concurrency: la misma regla que ordena tu memoria en un solo hilo te protege entre hilos.

fn main() {
    let mut datos = vec![1, 2, 3];
    let r1 = &datos;              // lector
    let r2 = &datos;              // otro lector a la vez: permitido
    println!("{r1:?} y {r2:?}");  // ...aqui terminan de usarse r1 y r2

    let w = &mut datos;           // ahora si: un unico escritor, sin lectores vivos
    w.push(4);
    // println!("{r1:?}");        // descomentar => error[E0502]: leer mientras se escribe
}

Coste cero: la garantía no está en el binario

Aquí se cierra la promesa. Todo lo anterior (ownership, préstamos, la regla de aliasing) es información para el compilador, no código que corra. Una vez demostrado que tu programa es seguro, esa demostración se descarta: en el binario no queda ni un contador de referencias, ni un recolector, ni una comprobación de límites que no hubieras pedido.

// Abstraccion de alto nivel, expresiva y segura...
let pares: u64 = (1u64..=1_000).filter(|n| n % 2 == 0).sum();

Ese iterador con su cierre filter no reserva memoria ni crea objetos: el compilador lo monomorfiza y, tras optimizar, lo reduce al mismo bucle contador que escribirías a mano en C. Es la definición de Bjarne Stroustrup de abstracción de coste cero: lo que no usas, no lo pagas; y lo que usas, no lo harías mejor a mano. Rust la extiende a la seguridad de memoria.

Demostrar en vez de vigilar: el salto conceptual

La diferencia entre Rust y un lenguaje con GC no es de grado, es de naturaleza. Un recolector de basura vigila tu memoria en tiempo de ejecución: gasta ciclos comprobando, una y otra vez mientras el programa corre, qué sigue vivo. Rust, en cambio, demuestra una sola vez, en compilación, que tu gestión de memoria es correcta, y entonces se aparta y te deja correr a toda velocidad. Es la diferencia entre un guardia que patrulla el edificio toda la noche y un arquitecto que prueba sobre el plano que el edificio no se puede caer. El primero cuesta para siempre; el segundo, una sola vez. Esta inversión (del runtime al compilador) es la idea que reorganiza todo lo demás en Rust: explica por qué la compilación es más lenta (el compilador trabaja más para que tú no pagues después), por qué los tipos son tan expresivos (son el lenguaje en el que escribes la demostración), y por qué “pelear con el borrow checker” no es burocracia sino la demostración tomando forma. Cuando entiendas que estás escribiendo una prueba de correctitud disfrazada de programa, Rust dejará de sorprenderte y empezará a tener un sentido inevitable.

⚔️ Verifica la promesa con tus manos
  1. Escribe el ejemplo de libro/copia y descomenta la línea del println! con el valor movido. Lee el error E0382 completo: el compilador te señala dónde se movió y dónde intentaste reusarlo.
  2. En el ejemplo de datos, descomenta el último println! y observa cómo un préstamo inmutable que “sigue vivo” choca con el &mut. Muévelo antes del préstamo mutable y verás que compila.
  3. Explica en voz alta, sin tecnicismos, la regla “muchos lectores o un escritor, nunca ambos”. Si puedes enseñarla, la entiendes.