Cuando escribes let s2 = s1 en Rust con un String, no obtienes dos copias del mismo dato. Obtienes un move: la propiedad del valor se transfiere a s2 y s1 queda invalidado. Esta es la move semantics de Rust, y entenderla es fundamental para trabajar con el lenguaje sin pelear contra el compilador.
Qué hay en la memoria cuando creas un String
Un String en Rust tiene tres campos en el stack:
- un puntero al dato en el heap
- la longitud actual
- la capacidad total reservada
let s1 = String::from("hola");
// Stack: { ptr ? heap, len: 4, capacity: 4 }
// Heap: h o l a
La asignación mueve, no copia
let s2 = s1;
// Stack s2: { ptr ? mismo heap, len: 4, capacity: 4 }
// s1 queda INVALIDADO
Rust copia los tres campos del stack de s1 a s2, pero no duplica el heap. Además invalida s1 para que nunca haya dos propietarios del mismo dato. Si ambos intentaran liberar la memoria al salir del scope, sería un double free, uno de los bugs más peligrosos en C.
fn main() {
let s1 = String::from("hola");
let s2 = s1;
println!("{}", s1); // ERROR
println!("{}", s2); // OK
}
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:5:20
|
2 | let s1 = String::from("hola");
| -- move occurs because `s1` has type `String`
3 | let s2 = s1;
| -- value moved here
5 | println!("{}", s1);
| ^^ value borrowed here after move
Tipos Copy: los que sí se copian
No todos los tipos se mueven. Los tipos que implementan el trait Copy se copian de forma implícita en lugar de moverse:
let x: i32 = 5;
let y = x; // copia, no move
println!("{}", x); // OK, x sigue válido
println!("{}", y); // OK
Los tipos Copy son los que viven completamente en el stack y tienen tamaño conocido en tiempo de compilación:
- Enteros:
i8,i16,i32,i64,i128,isize,u8 - Flotantes:
f32,f64 boolchar- Tuplas cuyos elementos son todos
Copy:(i32, bool) - Arrays de tipos
Copy:[i32; 4]
String, Vec, Box y cualquier tipo con datos en el heap no son Copy.
Move en llamadas a funciones
fn imprimir(s: String) {
println!("{}", s);
} // s se destruye aquí
fn main() {
let msg = String::from("hola");
imprimir(msg); // move: msg ya no es válido
// println!("{}", msg); // ERROR
}
Pasar un valor a una función es semánticamente idéntico a asignarlo. Si quieres conservar la propiedad, tienes dos opciones: devolver el valor desde la función o usar borrowing (referencias).
Clone: cuando necesitas una copia real
Si realmente necesitas dos copias independientes del dato en el heap, usa .clone():
let s1 = String::from("hola");
let s2 = s1.clone(); // duplica el heap
println!("{}", s1); // OK
println!("{}", s2); // OK
.clone() es una operación explícita y potencialmente cara. Rust te hace escribirla a mano para que seas consciente de que estás duplicando datos.
Move en enums y structs
struct Punto {
x: f64,
y: f64,
}
struct Mensaje {
contenido: String,
}
fn main() {
let p1 = Punto { x: 1.0, y: 2.0 };
let p2 = p1; // Punto es Copy si sus campos son Copy
pero no lo es por defecto
// Necesitarías #[derive(Copy, Clone)] para que funcione sin move
let m1 = Mensaje { contenido: String::from("hola") };
let m2 = m1; // move: m1 queda invalidado
}
Resumen
- La asignación en Rust mueve los valores que no son
Copy. - El original queda invalidado: no hay double free ni aliasing accidental.
- Los tipos simples del stack (
i32,bool,char) implementanCopyy se copian automáticamente. - Para duplicar un tipo no
Copy, usa.clone()de forma explícita.
Una vez que entiendes que la asignación mueve, el siguiente paso es borrowing: cómo pasar referencias a datos sin transferir la propiedad, que es el mecanismo que usarás en la gran mayoría del código Rust real.
