← Pergaminos rust

La forma más sencilla de entender el ownership en Rust

Por qué Rust se lleva un valor que acabas de pasar - y cómo dejar de luchar contra ello

La forma más sencilla de entender el ownership en Rust

Esta traducción fue asistida por IA y revisada superficialmente. Si encuentras errores o algo no suena natural, escríbeme - lo agradezco.

Ya has escrito funciones. Sabes cómo pasar un valor. Lo has hecho en Python, en JavaScript, en Go

Entonces Rust te dice que el valor ya no existe.

Sin advertencia previa. Sin excepción que puedas capturar. Solo un error del compilador apuntando a una línea que parece completamente normal - y una palabra que no habías visto usada así antes: moved.

No lo borraste. No lo liberaste. Lo pasaste a una función. Y Rust dice que ya no existe.

¿ Por qué Rust funciona así ?

Todo programa usa memoria. Y en algún momento, esa memoria hay que limpiarla.

En la mayoría de los lenguajes que has usado, algo más se encarga de eso por ti - en silencio, en segundo plano, a su propio ritmo. Funciona. Pero tiene un coste: nunca sabes exactamente cuándo ocurre, y el entorno de ejecución siempre corre junto a tu programa haciendo ese trabajo.

Rust toma un camino diferente. En lugar de limpiar en tiempo de ejecución, calcula las reglas de limpieza en tiempo de compilación - antes de que tu programa llegue a ejecutarse. Sin proceso en segundo plano. Sin suposiciones. El compilador sabe exactamente cuándo se deja de usar cada valor, y se encarga del resto.

El ownership es el mecanismo que lo hace posible.

La llave

Piensa en una llave física. Una copia, un portador. Puedes prestarla una tarde, pero mientras está en el bolsillo de otro no puedes entrar a la habitación. Puedes entregarla definitivamente - pero en el momento en que lo haces, la puerta queda cerrada para ti. El compilador sabe exactamente dónde está cada llave. Nunca adivina. Nunca olvida.

El modelo equivocado

En la mayoría de los lenguajes, pasar un valor a una función es como mostrarle un documento a alguien: todavía lo tienes. Lo ve, lo usa, y tú sigues adelante. Nada cambia de manos. Ese modelo es razonable. Así funciona Python. Así funciona JavaScript. Pero Rust no funciona así .

El problema

Aquí hay código que escribirías naturalmente:

fn print_task(t: String) {
    println!("Task: {}", t);                       // el valor se usa y se descarta
}

fn main() {
    let title = String::from("Buy coffee beans"); // la llave
    print_task(title);                            // la llave se entrega para procesarla
    println!("Reminder: {}", title);              // todavía es mía, ¿no?
}

El compilador no está de acuerdo:

error[E0382]: borrow of moved value: `title`
 --> src/main.rs:4:28
  |
2 |     let title = String::from("Buy coffee beans");
  |         ----- move occurs because `title` has type `String`,
  |               which does not implement the `Copy` trait
3 |     print_task(title);
  |                ----- value moved here
4 |     println!("Reminder: {}", title);
  |                              ^^^^^ value borrowed here after move

No dice “olvidaste copiarlo.” Dice: moved here. La palabra es deliberada.

Una llave, un portador

Cuando pasaste title a print_task, entregaste la llave. No una copia - la llave misma. La función ahora es la dueña de ese String. Es responsable de él. Cuando print_task termina, el valor se descarta: limpiado, desaparecido. Para cuando llegas al println!, la habitación ya está cerrada y vacía.

Esto es el ownership: un valor, un dueño, limpieza automática cuando el dueño sale del alcance. El compilador lo verifica de forma estática - antes de que el programa corra, con costo cero en tiempo de ejecución.

Algunos valores se comportan diferente.

let y = x con un entero funciona bien - los números son pequeños, de tamaño fijo, y copiarlos es barato. Rust los copia automáticamente.

Los strings no. Un String puede ser grande y mutable. Copiarlo no siempre es barato, así que Rust no lo hace a tus espaldas.

Cuando el dueño se va

El ownership está ligado al alcance. Cuando el dueño sale del alcance - cuando el bloque termina, cuando la función retorna - el valor se descarta automáticamente.

fn main() {
    {
        let title = String::from("Buy coffee beans");
        // title es válido aquí
    }
    // title desapareció. la habitación está cerrada. la llave ya no existe.
}

Esto no es una limitación. Es la garantía. Siempre sabes exactamente cuándo desaparece un valor. Sin sorpresas. Sin limpieza que recordar.

El artesano

Ahora considera una función que toma ownership y devuelve algo:

fn make_urgent(t: String) -> String {
    t + " (!)"                         // la llave vieja se funde, se devuelve una nueva
}

fn main() {
    let title = String::from("Buy coffee beans"); // la llave
    let title = make_urgent(title);               // la llave se entrega al artesano
    println!("Task: {}", title);                  // el mismo gancho, otra llave
}

make_urgent toma la llave, la transforma, y devuelve una nueva. El String original fue la materia prima - consumida en el proceso. El nuevo binding (let title = ...) cuelga el resultado en el mismo gancho con la misma etiqueta. El mismo gancho. Otra llave.

Cuando necesitas dos llaves

A veces dos dueños genuinamente necesitan copias independientes. Para eso, Rust tiene .clone():

fn main() {
    let title = String::from("Buy coffee beans");
    let backup = title.clone();          // una segunda llave, completamente duplicada

    print_task(title);
    println!("Backup: {}", backup);      // backup sigue siendo válido
}

Clone es explícito por diseño. Cuando lo ves, sabes que se está haciendo una copia completa - y que tiene un costo. Rust nunca copia valores complejos automáticamente. Si no lo pides, no ocurre.

Lo que cambia

Una vez que el modelo está en su lugar, algo cambia. Los mensajes de error no cambian - pero tú sí.

“Value moved here” ya no es el compilador siendo difícil. Es el compilador diciéndote exactamente dónde cambió de manos la llave. “Borrow of moved value” no es una queja. Es un mapa.

Las reglas no se volvieron más simples. Pero dejaron de sentirse arbitrarias. Cada error apunta a un momento real - una llave que se movió, una habitación que se cerró. El compilador no está adivinando. Nunca lo estuvo.

Las tres reglas

Todo lo anterior se deriva de tres reglas. Ya las has visto todas - aquí están con la llave en mente:

Eso es el ownership. No una restricción - un contrato. Uno que el compilador hace cumplir para que tú no tengas que hacerlo.


Una cosa más que vale saber: si una función solo necesita leer un valor - no poseerlo - Rust te permite prestar la llave temporalmente. Una referencia (&String) es la llave entregada por la duración de la llamada, devuelta automáticamente cuando la función termina. La habitación sigue siendo tuya. El ownership es la transferencia permanente. El préstamo no lo es. El borrow checker es la regla que gobierna el préstamo - y es otra pared, para otro artículo.