Ownership en Rust
El ownership (propiedad) es la idea que hace que Rust sea Rust. Es la forma en que el lenguaje garantiza que la memoria se usa de forma segura sin un recolector de basura (garbage collector) y sin que tengas que liberar la memoria a mano. Al principio se siente raro, pero la idea central es sencilla. Vamos a construirla poco a poco.
El problema que resuelve el ownership
Sección titulada «El problema que resuelve el ownership»Mientras un programa se ejecuta, guarda datos en la memoria. En algún momento esa memoria hay que limpiarla; si no, el programa va acumulando memoria que ya no usa (una “fuga de memoria”) y, tarde o temprano, falla. Los lenguajes suelen resolver esto de dos maneras:
- A mano (C/C++): tú liberas la memoria. Si se te olvida, hay fugas. Si la liberas demasiado pronto, aparecen fallos y agujeros de seguridad.
- Con recolector de basura (Java, Go, Python): un proceso en segundo plano limpia por ti. Es cómodo, pero cuesta rendimiento y hace que el programa sea menos predecible.
Rust toma un tercer camino: decide al compilar exactamente cuándo hay que limpiar cada valor, siguiendo unas reglas de ownership. No liberas nada a mano y no hay recolector de basura.
Las tres reglas del ownership
Sección titulada «Las tres reglas del ownership»Estas tres reglas son todo el sistema. Lo demás sale de ellas:
- Cada valor tiene un único dueño (owner): la variable que lo “posee”.
- Solo puede haber un dueño a la vez.
- Cuando el dueño sale de su ámbito (scope), el valor se limpia automáticamente.
Veamos cada regla en acción.
Regla 3: los valores se liberan al final del ámbito
Sección titulada «Regla 3: los valores se liberan al final del ámbito»Un ámbito (scope) es la zona que hay entre { y }. Cuando termina el ámbito de una variable, Rust libera su valor automáticamente. Nunca escribes free():
fn main() { { let message = String::from("hola"); // message es válida aquí println!("{message}"); } // <- termina el ámbito: `message` se limpia automáticamente
// println!("{message}"); // ❌ error: aquí message ya no existe}No tuviste que liberar nada: Rust lo hizo en el momento exacto en que message salió de su ámbito.
Reglas 1 y 2: el “move”
Sección titulada «Reglas 1 y 2: el “move”»Aquí viene lo interesante. Mira qué pasa cuando asignas una variable a otra:
fn main() { let s1 = String::from("hola"); let s2 = s1; // el ownership SE MUEVE de s1 a s2
println!("{s2}"); // ✅ funciona // println!("{s1}"); // ❌ error: s1 se movió}Cuando escribes let s2 = s1, Rust no copia el texto: mueve (move) el ownership de s1 a s2. Piensa en una llave de casa: no se hace una copia, se la pasas a otra persona. Como la regla 2 dice que solo puede haber un dueño, s1 deja de ser válida. Si intentas usarla, obtienes este error:
error[E0382]: borrow of moved value: `s1`El compilador te está diciendo que intentaste usar s1 después de que su valor se movió a otra variable.
¿Y si quiero dos copias?
Sección titulada «¿Y si quiero dos copias?»Si de verdad quieres una segunda copia, independiente de la primera, llama a .clone():
fn main() { let s1 = String::from("hola"); let s2 = s1.clone(); // hace una copia completa
println!("s1 = {s1}, s2 = {s2}"); // ✅ ahora las dos funcionan}clone() es explícito a propósito: copiar puede ser costoso, así que Rust quiere que lo pidas tú.
Los tipos simples se copian, no se mueven
Sección titulada «Los tipos simples se copian, no se mueven»Quizás te preguntes por qué esto no da error:
fn main() { let x = 5; let y = x; println!("x = {x}, y = {y}"); // ✅ las dos funcionan}Los valores pequeños y de tamaño fijo, como los enteros, los números decimales (f64), los booleanos y char, son baratos de copiar, así que Rust simplemente los copia en lugar de moverlos. Si estás empezando, quédate con esta regla práctica:
- Valores simples (números,
bool,char) → se copian. La variable original sigue siendo válida. - Datos con dueño (como
String) → se mueven. La variable original deja de ser válida.
Ownership y funciones
Sección titulada «Ownership y funciones»Pasarle un valor a una función también lo mueve, igual que cuando lo asignas a otra variable:
fn take_ownership(s: String) { println!("Ahora el dueño soy yo: {s}");} // aquí s se libera
fn main() { let text = String::from("hola"); take_ownership(text); // println!("{text}"); // ❌ error: text se movió a la función}Justo por esto existe el siguiente tema: el borrowing (préstamo). El borrowing permite que una función use un valor sin quedarse con él, para que tú puedas seguir usándolo después.
Práctica
Sección titulada «Práctica»Intenta resolver cada ejercicio por tu cuenta primero, en el Playground de Rust o en un proyecto local. Es normal que aparezcan errores del compilador por el camino: léelos, porque casi siempre señalan justo el lugar del move (movimiento).
1. Conserva el original
Sección titulada «1. Conserva el original»Este programa no compila. Arréglalo para que imprima borrador -> borrador v2, sin eliminar ninguna de las dos variables.
fn main() { let original = String::from("borrador"); let mut edited = original; edited.push_str(" v2"); println!("{original} -> {edited}");}error[E0382]: borrow of moved value: `original` --> src/main.rs:5:16 |2 | let original = String::from("borrador"); | -------- move occurs because `original` has type `String`, which does not implement the `Copy` trait3 | let mut edited = original; | -------- value moved here4 | edited.push_str(" v2");5 | println!("{original} -> {edited}"); | ^^^^^^^^ value borrowed here after moveEl compilador te dice que original se movió en la línea 3 (“value moved here”) y que después intentas usarlo en la línea 5.
Pista: necesitas dos strings independientes, para que cambiar uno no afecte al otro.
Ver solución
fn main() { let original = String::from("borrador"); let mut edited = original.clone(); // una copia completa e independiente edited.push_str(" v2"); println!("{original} -> {edited}");}Salida:
borrador -> borrador v2let mut edited = original; mueve el string, así que original deja de ser válido. .clone() crea un segundo string con su propia memoria. Ahora push_str solo cambia edited, y original sigue diciendo borrador.
2. Devuélvelo
Sección titulada «2. Devuélvelo»Escribe una función add_signature que tome el ownership de un String, le añada " -- Ferris" al final y devuelva el string. Llámala desde main e imprime el resultado.
Pista: una función puede devolver el valor que recibió. Escribe -> String en la firma y recibe el parámetro como mut text: String para poder modificarlo.
Ver solución
fn add_signature(mut text: String) -> String { text.push_str(" -- Ferris"); text // el ownership vuelve a salir hacia quien llamó}
fn main() { let letter = String::from("¡Hasta pronto!"); let letter = add_signature(letter); println!("{letter}");}Salida:
¡Hasta pronto! -- FerrisEl ownership entra en la función con la llamada y vuelve a salir con el valor de retorno. Lo recogemos en una nueva variable letter (que oculta a la anterior, lo que se llama shadowing). Funciona, pero pasar valores de ida y vuelta todo el rato acaba cansando. Justo ese es el problema que resuelve el borrowing.
3. ¿Quién es el dueño ahora?
Sección titulada «3. ¿Quién es el dueño ahora?»Este programa no compila. Arréglalo para que imprima Ana se unió a la lista y después ["Ana"].
fn main() { let mut names = Vec::new(); let name = String::from("Ana"); names.push(name); println!("{name} se unió a la lista"); println!("{:?}", names);}error[E0382]: borrow of moved value: `name` --> src/main.rs:5:16 |3 | let name = String::from("Ana"); | ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait4 | names.push(name); | ---- value moved here5 | println!("{name} se unió a la lista"); | ^^^^ value borrowed here after movePista: push toma el ownership, igual que una llamada a función. ¿Quién es el dueño del string después de la línea 4? O usas name antes del move, o se lo pides al nuevo dueño.
Ver solución
fn main() { let mut names = Vec::new(); let name = String::from("Ana"); println!("{name} se unió a la lista"); // lo usamos mientras todavía es nuestro names.push(name); // ahora el dueño es el vector println!("{:?}", names);}Salida:
Ana se unió a la lista["Ana"]Después de names.push(name), el dueño es el vector y name ya no se puede usar. Imprimir primero es una solución. Otra es mantener el orden original e imprimir names[0], porque el vector sigue teniendo el string. Clonarlo también funcionaría, pero aquí no hace falta copiar nada.
Resumen
Sección titulada «Resumen»- El ownership permite que Rust gestione la memoria de forma segura sin recolector de basura.
- Tres reglas: cada valor tiene un dueño, solo un dueño a la vez, y el valor se libera cuando su dueño sale del ámbito.
- Asignar datos con dueño (como un
String) los mueve: la variable original deja de ser válida. - Usa
.clone()para obtener una copia independiente; los tipos simples (números,bool,char) se copian solos. - Pasar un valor a una función también lo mueve, y eso nos lleva al borrowing.
Comprueba lo que aprendiste
Sección titulada «Comprueba lo que aprendiste»Comprueba lo aprendido
1. En Rust, ¿cuántos dueños puede tener un valor al mismo tiempo?
2. ¿Qué método llamas para hacer una copia independiente de un String en lugar de moverlo?
3. ¿Cuál de estos tipos se copia automáticamente, en lugar de moverse, al asignarlo a otra variable?
4. ¿Qué pasa cuando le pasas un String a una función que lo recibe por valor (no por referencia)?
5. Según esta página, ¿cuándo limpia (libera) Rust un valor automáticamente?
Puntuación: 0 / 5
Para leer más
Sección titulada «Para leer más»- ¿Qué es el ownership? — El libro de Rust (en inglés): el capítulo oficial, con dibujos de cómo es un
Stringen memoria y por qué un move no lo copia. - Ownership and moves — Rust by Example (en inglés): ejemplos cortos y ejecutables de moves, incluido mover valores a funciones.
- RAII — Rust by Example (en inglés): cómo se liberan los valores cuando salen del ámbito y qué papel juega el trait
Drop. Copy— documentación de la biblioteca estándar (en inglés): qué tipos se copian en lugar de moverse, y la diferencia entreCopyyClone.- Rust track — Exercism (en inglés): ejercicios gratuitos con comentarios sobre tu solución, para cuando quieras practicar más de lo que ofrece esta página.