Ownership: Movimientos, Copy vs Clone
La idea más audaz de Rust es que cada valor tiene exactamente un dueño — entender qué pasa cuando ese dueño cambia abre el resto del lenguaje.
Todo valor que creas en Rust necesita un lugar donde vivir, y en algún momento necesita dejar de vivir ahí — alguien tiene que liberar esa memoria. Los lenguajes con recolector de basura resuelven esto escaneando en tiempo de ejecución en busca de valores inalcanzables; C y C++ lo resuelven confiando en que tú llames a free() en el momento exacto, que es justo la confianza que produce errores de use-after-free y double-free. La respuesta de Rust es el ownership (propiedad), y se aplica completamente en tiempo de compilación mediante tres reglas simples: cada valor tiene exactamente un dueño en todo momento; cuando ese dueño sale de scope, el valor se libera (drop); y la propiedad puede moverse de una variable a otra, pero nunca puede compartirse sin tu permiso explícito.
Vale la pena detenerse en la regla dos, porque es lo que reemplaza tanto al recolector de basura como a la llamada manual a free(). Cuando una variable sale de scope — cuando termina el bloque, la función o el {} que la contiene — Rust llama automáticamente a un método especial llamado drop sobre su valor, que libera lo que sea que ese valor posea: memoria del heap, un manejador de archivo, un socket de red. Nunca escribes esa llamada tú mismo; el compilador la inserta de forma determinista, en un punto que puede probar que es correcto, y por eso el código Rust no tiene pausas de GC ni necesita un bloque finally solo para liberar un recurso.
Ahora piensa en qué pasa cuando asignas una variable a otra. Si s1 es un String — un texto de tamaño variable alojado en el heap — y escribes let s2 = s1;, Rust no copia el buffer del heap. Transfiere la propiedad de s1 a s2, y a partir de ahí trata a s1 como ya no válida deliberadamente: cualquier intento de usar s1 después es un error de compilación, no una sorpresa en tiempo de ejecución. Esta es la diferencia crucial frente a una copia superficial ingenua en C, donde ambas variables terminarían apuntando a la misma memoria del heap — y cuando ambas eventualmente salen de scope, esa memoria se libera dos veces, corrompiendo tu programa. La semántica de movimiento (move) de Rust hace que ese escenario ni siquiera pueda compilar.
Entonces, ¿por qué un código que se ve idéntico se comporta distinto con i32? Porque i32 implementa un trait marcador especial llamado Copy. Los tipos que son pequeños, de tamaño fijo y viven completamente en el stack — enteros, floats, bool, char, y tuplas de tipos Copy — se suman a Copy, lo que significa que la asignación duplica los bits en vez de mover la propiedad. Tanto el original como la variable nueva quedan válidos y utilizables de forma independiente, porque duplicar unos pocos bytes del stack es prácticamente gratis y no hay ningún recurso del heap compartido que dos dueños puedan liberar dos veces. String, Vec<T> y la mayoría de los tipos que poseen memoria en el heap no pueden implementar Copy, justamente porque una duplicación barata a nivel de bits dejaría a dos dueños responsables de liberar la misma memoria.
Cuando realmente quieres dos copias independientes y con propiedad completa de datos en el heap — no solo leer lo mismo dos veces, que es para lo que sirven las referencias en la próxima lección — Rust te obliga a pedirlo explícitamente con .clone(). Es una decisión de diseño deliberada: copiar en profundidad un String o un Vec grande tiene un costo real en tiempo de ejecución, a veces considerable, y Rust nunca quiere que un costo así sea invisible en tu código fuente. Si ves .clone() en el código de alguien, sabes de inmediato que se hizo una copia real e independiente a propósito — nunca tienes que preguntarte si un = de apariencia inocente hizo lo mismo en secreto.
Intentar usar un valor después de que fue movido es uno de los primeros errores del compilador que se encuentra cualquiera que aprende Rust, así que vale la pena verlo con claridad. Dado let s1 = String::from("hello"); let s2 = s1;, cualquier línea posterior que lea s1 falla con error[E0382]: borrow of moved value: s1, porque la propiedad ya se transfirió a s2 en la línea anterior. La solución depende de lo que realmente necesites: si solo pensabas usar un nombre para el valor, simplemente usa s2 de ahí en adelante; si de verdad necesitas que s1 y s2 sigan siendo válidos e independientes, llama a s1.clone() en vez de mover, aceptando el costo de una copia profunda explícita a cambio de tener dos dueños.
Hay otro lugar donde la propiedad se mueve silenciosamente: en el límite de una llamada a función. Pasar un String por valor a una función mueve su propiedad al parámetro de esa función, y quien la llamó pierde el acceso después, exactamente como si hubieras escrito tú mismo let param = mi_string;. Muchas veces eso no es lo que quieres — la mayoría de las funciones solo necesitan leer o usar brevemente un valor, no quedarse con su propiedad para siempre — y clonar todo solo para evitar un move sería un desperdicio y ocultaría costos de asignación reales por todos lados. Esa tensión es exactamente lo que la próxima lección, referencias y borrowing, existe para resolver.
fn main() {{let s = String::from("hello");println!("{} is alive here", s);}println!("s is no longer accessible in this scope");}
The inner block gives s its own scope; the moment that block ends, Rust calls drop on s automatically and frees its heap buffer, with no garbage collector involved.
fn main() {let a = 5;let b = a;println!("a = {}, b = {}", a, b);let s1 = String::from("hello");let s2 = s1;println!("s2 = {}", s2);}
i32 implements Copy, so a and b are both independently valid after the assignment; String does not, so the second assignment moves ownership from s1 into s2 instead of copying it.
fn main() {let s1 = String::from("hello");let s2 = s1;// Using s1 here would fail to compile:// println!("{}, world!", s1);// error[E0382]: borrow of moved value: `s1`// s1's ownership moved to s2 on the line above, so s1 is no longer valid.println!("{}, world!", s2);let s3 = String::from("hello");let s4 = s3.clone();println!("s3 = {}, s4 = {}", s3, s4);}
The commented-out line shows the use-after-move error you would get from reading s1 after it moved into s2; the working fix below it uses .clone() to create two fully independent Strings on purpose.
🧠 Comprueba tu comprensión
0/1 · 0/1 answered1. Why does assigning let s2 = s1; move ownership when s1 is a String, but assigning let b = a; simply copies the value when a is an i32?