Ownership en Rust: las tres reglas que cambian cómo piensas sobre la memoria

Rust es conocido por su seguridad de memoria sin recolector de basura. El mecanismo que lo hace posible se llama ownership (propiedad). No es un runtime check ni una heurística del compilador: es un conjunto de tres reglas que el compilador verifica en tiempo de compilación y que, una vez que las interiorices, cambian la forma en que razonas sobre cualquier programa.

Las tres reglas del ownership

Las reglas son simples de enunciar:

  1. Cada valor en Rust tiene un propietario (owner).
  2. Solo puede haber un propietario a la vez.
  3. Cuando el propietario sale del scope, el valor se destruye.

Todo lo que garantiza Rust sobre memoria se deriva de estas tres reglas. Vamos a ver qué significa cada una con código real.

Regla 1: cada valor tiene un propietario

fn main() {
    let s = String::from("hola"); // s es el propietario de este String
    println!("{}", s);
}

Aquí s es el propietario del String "hola". No hay ambigüedad: exactamente una variable posee ese dato en memoria.

Regla 2: un único propietario a la vez

fn main() {
    let s1 = String::from("hola");
    let s2 = s1; // la propiedad se MUEVE a s2

    // println!("{}", s1); // ERROR: s1 ya no es válido
    println!("{}", s2);    // OK
}

Cuando asignas s1 a s2, la propiedad se transfiere. s1 queda invalidado. Si intentas usarlo, el compilador produce:

error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:20
  |
2 |     let s1 = String::from("hola");
  |         -- move occurs because `s1` has type `String`
3 |     let s2 = s1;
  |              -- value moved here
5 |     println!("{}", s1);
  |                    ^^ value borrowed here after move

Esto evita el double free: si tanto s1 como s2 apuntaran al mismo dato y ambos intentaran liberarlo al salir del scope, el programa corrompería la memoria. Rust lo impide estructuralmente.

Regla 3: el valor se destruye al salir del scope

fn main() {
    {
        let s = String::from("dentro"); // s entra en scope
        println!("{}", s);
    } // s sale del scope ? Rust llama a drop() automáticamente

    // println!("{}", s); // ERROR: s no existe aquí
}

Rust inserta una llamada implícita a drop() al final de cada scope. Eso es todo lo que necesita para liberar memoria. Sin GC, sin contador de referencias, sin nada en runtime.

Ownership y funciones

Pasar un valor a una función transfiere la propiedad exactamente igual que una asignación:

fn tomar_propiedad(s: String) {
    println!("{}", s);
} // s se destruye aquí

fn hacer_copia(n: i32) {
    println!("{}", n);
} // n se destruye, pero era una copia, no importa

fn main() {
    let s = String::from("hola");
    tomar_propiedad(s); // la propiedad se mueve a la función
    // println!("{}", s); // ERROR

    let n = 5;
    hacer_copia(n); // i32 implementa Copy, n sigue válido
    println!("{}", n); // OK
}

Devolver la propiedad desde una función

Una función también puede devolver la propiedad:

fn crear_string() -> String {
    String::from("creado aquí") // la propiedad se mueve al caller
}

fn tomar_y_devolver(s: String) -> String {
    s // la propiedad vuelve al caller
}

fn main() {
    let s1 = crear_string();       // s1 recibe la propiedad
    let s2 = tomar_y_devolver(s1); // s1 se mueve; s2 recibe de vuelta
    println!("{}", s2);
}

Este patrón existe, pero es verboso. En la práctica usarás borrowing (referencias) para pasar datos sin transferir la propiedad, que veremos en el siguiente artículo.

Por qué no hay fugas de memoria

Un objeto solo puede existir mientras su propietario esté en scope. Cuando el propietario se destruye, el objeto también. No hay forma de "olvidar" liberar memoria porque la liberación es automática y está garantizada por el compilador.

Tampoco hay use-after-free: una vez que el propietario sale de scope, cualquier intento de usar el valor es un error de compilación. No hay acceso en runtime que pueda fallar silenciosamente.

Resumen

  • Cada valor tiene un único propietario.
  • Asignar o pasar a una función mueve la propiedad.
  • Al salir del scope, el valor se destruye automáticamente.
  • Esto elimina fugas de memoria y double free sin recolector de basura.

Con estas tres reglas, Rust garantiza seguridad de memoria en tiempo de compilación. El siguiente paso es entender move semantics en detalle: qué ocurre exactamente cuando mueves un valor y por qué algunos tipos como i32 no se mueven sino que se copian.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP