Ir al contenido

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.

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.

Estas tres reglas son todo el sistema. Lo demás sale de ellas:

  1. Cada valor tiene un único dueño (owner): la variable que lo “posee”.
  2. Solo puede haber un dueño a la vez.
  3. 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
}
▶ Pruébalo tú mismo

No tuviste que liberar nada: Rust lo hizo en el momento exacto en que message salió de su ámbito.

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ó
}
▶ Pruébalo tú mismo

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.

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
}
▶ Pruébalo tú mismo

clone() es explícito a propósito: copiar puede ser costoso, así que Rust quiere que lo pidas tú.

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.

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.

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).

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` trait
3 | let mut edited = original;
| -------- value moved here
4 | edited.push_str(" v2");
5 | println!("{original} -> {edited}");
| ^^^^^^^^ value borrowed here after move

El 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 v2
▶ Pruébalo tú mismo

let 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.

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! -- Ferris
▶ Pruébalo tú mismo

El 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.

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` trait
4 | names.push(name);
| ---- value moved here
5 | println!("{name} se unió a la lista");
| ^^^^ value borrowed here after move

Pista: 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"]
▶ Pruébalo tú mismo

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.

  • 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 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