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
- y siempre se sintió igual: pasas algo, y todavía lo tienes. Ese es el modelo al que estás acostumbrado.
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:
- Cada valor tiene un dueño. Una llave. Un portador.
- Solo puede haber un dueño a la vez. La entregas, ya no la tienes.
- Cuando el dueño sale del alcance, el valor se descarta. El dueño se va, la habitación queda cerrada.
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.