3. Ownership y movimientos
Ejemplo completo: examples/l03_ownership.rs — cargo run --example l03_ownership.
El problema que resuelve un GC
Sección titulada «El problema que resuelve un GC»En C# y Java, los objetos viven en el montón (heap) y varias variables pueden apuntar al mismo objeto. Nadie lo «posee»: el recolector de basura (garbage collector) lo libera en algún momento después de que desaparezca la última referencia.
Es cómodo, pero cuesta un runtime, pausas y memoria de margen — y solo gestiona la memoria: los archivos, los sockets y los locks siguen necesitando using / IDisposable o try-with-resources.
Rust no tiene GC. En su lugar, el compilador impone reglas de propiedad (ownership) e inserta él mismo el código de limpieza, en tiempo de compilación.
Las tres reglas
Sección titulada «Las tres reglas»- Cada valor tiene exactamente un propietario (una variable, un campo, un elemento de una colección…).
- Cuando el propietario sale de su ámbito, el valor se libera (drop).
- La propiedad se puede mover a otro propietario; el propietario anterior ya no se puede usar.
Movimientos (moves)
Sección titulada «Movimientos (moves)»let a = String::from("hello");let b = a; // la propiedad del buffer del montón pasa a bprintln!("b = {b}"); // b = helloEn C#, var b = a; copia una referencia: a y b apuntan ahora a la misma cadena, y ambas siguen siendo utilizables. En Rust, a ya no existe:
let a = String::from("hello");let b = a;println!("{a} {b}");error[E0382]: borrow of moved value: `a` --> e_move.rs:4:16 |2 | let a = String::from("hello"); | - move occurs because `a` has type `String`, which does not implement the `Copy` trait3 | let b = a; | - value moved here4 | println!("{a} {b}"); | ^ value borrowed here after move |help: consider cloning the value if the performance cost is acceptable |3 | let b = a.clone(); | ++++++++¿Por qué? Si a y b poseyeran ambos el buffer, ambos lo liberarían al final del ámbito — una doble liberación. El movimiento hace que «quién libera esto» no sea ambiguo.
clone — una copia profunda explícita
Sección titulada «clone — una copia profunda explícita»let c = b.clone();println!("b = {b}, c = {c}"); // b = hello, c = helloclone() duplica los datos del montón. Siempre es visible en el código, así que las copias costosas nunca ocurren por accidente.
Los tipos Copy
Sección titulada «Los tipos Copy»Los valores pequeños que viven por completo en la pila se copian en lugar de moverse:
let x = 5;let y = x;println!("x = {x}, y = {y}"); // x = 5, y = 5Los enteros, los flotantes, bool, char, y las tuplas y arrays formados por ellos son Copy. Se parece a los tipos de valor de C# (struct) — pero en Rust tus propios structs se mueven por defecto y solo pasan a ser Copy si lo pides con #[derive(Clone, Copy)].
| C# | Java | Rust | |
|---|---|---|---|
b = a con un objeto del montón |
ambos referencian el mismo objeto | ambos referencian el mismo objeto | movimiento: a inutilizable |
b = a con un int |
copia | copia | copia (tipo Copy) |
| copia profunda explícita | ICloneable, constructor de copia |
clone(), constructor de copia |
.clone() |
Las funciones también toman la propiedad
Sección titulada «Las funciones también toman la propiedad»Pasar un String por valor lo mueve a la función:
fn take(s: String) -> usize { s.len()} // s se libera aquí
let name = String::from("Ferris");let len = take(name);println!("{name} has {len} letters");error[E0382]: borrow of moved value: `name` --> e_move_fn.rs:8:16 |6 | let name = String::from("Ferris"); | ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait7 | let len = take(name); | ---- value moved here8 | println!("{name} has {len} letters"); | ^^^^ value borrowed here after move |note: consider changing this parameter type in function `take` to borrow instead if owning the value isn't necessaryEl compilador ya señala la verdadera solución: tomar prestado en lugar de tomar la propiedad. Ese es el tema de la lección 4.
Devolver un valor devuelve la propiedad a quien llama:
fn make_greeting(name: &str) -> String { format!("Hello, {name}!")}
let greeting = make_greeting("Ferris"); // greeting posee el nuevo StringDrop — limpieza determinista
Sección titulada «Drop — limpieza determinista»Cuando un propietario sale de su ámbito, Rust llama a drop, en orden inverso al de declaración. Puedes engancharte a ello implementando el trait Drop:
struct TempFile { name: String,}
impl Drop for TempFile { fn drop(&mut self) { println!("dropping {}", self.name); }}
fn main() { let _first = TempFile { name: "first.tmp".into() }; { let _inner = TempFile { name: "inner.tmp".into() }; println!("leaving inner scope"); } let _second = TempFile { name: "second.tmp".into() }; println!("end of main");}leaving inner scopedropping inner.tmpend of maindropping second.tmpdropping first.tmp| C# | Java | Rust | |
|---|---|---|---|
| Memoria | GC, no determinista | GC, no determinista | se libera al final del ámbito del propietario |
| Archivos, sockets, locks | using + IDisposable |
try-with-resources + AutoCloseable |
el mismo Drop, automáticamente |
| Olvidar la limpieza | fuga hasta el finalizador (quizá) | fuga hasta el finalizador (quizá) | la limpieza se ejecuta automáticamente; una fuga requiere un std::mem::forget explícito o un ciclo de referencias (lección 11) |
Este patrón — adquirir en un constructor, liberar en Drop — es como funcionan File, MutexGuard y las conexiones de red en Rust. No hay palabra clave using porque cada ámbito ya se comporta como uno.
Puntos clave
Sección titulada «Puntos clave»- Un propietario por valor; el valor se libera cuando el propietario sale de su ámbito.
- Asignar o pasar un valor que no es
Copylo mueve; la variable anterior queda inutilizable. .clone()es la copia profunda explícita y visible.Dropofrece una limpieza determinista para la memoria y los recursos, como unusingautomático.
Ejercicios
Sección titulada «Ejercicios»- ¿Qué líneas compilan? Explica cada una.
let a = 10;let b = a;println!("{a}"); // (1)
let s = String::from("x");let t = s;println!("{s}"); // (2)
let u = String::from("y");let v = u.clone();println!("{u} {v}"); // (3)Solución
- Compila:
i32esCopy, así quebrecibe una copia yasigue siendo utilizable. - No compila (
E0382):Stringno esCopy, así quesse movió at. - Compila:
clone()crea unStringindependiente, así que ambos siguen siendo válidos.
- Reescribe este método de Java para que la versión en Rust no necesite
clone():
static int countVowels(String text) { /* … */ }// se llama así: countVowels(name); System.out.println(name);Solución
Recibe un slice de cadena prestado en lugar de un String en propiedad, para que quien llama conserve la propiedad (se explica en la lección 4):
fn count_vowels(text: &str) -> usize { text.chars().filter(|c| "aeiouAEIOU".contains(*c)).count()}
let name = String::from("Ferris");let n = count_vowels(&name);println!("{name}: {n} vowels");- ¿En qué orden se liberan
a,byc?
let a = TempFile { name: "a".into() };let b = TempFile { name: "b".into() };let c = TempFile { name: "c".into() };drop(b);println!("done");Solución
Primero b (explícitamente, mediante std::mem::drop, antes de que se imprima done), luego, al final del ámbito, c y después a — orden inverso al de declaración para los valores que aún tienen propietario.