Move semantics: la propiedad se mueve
Al asignar o pasar un valor no-Copy como `String`, la propiedad se transfiere y el original deja de ser válido. Por qué Rust lo hace, qué es exactamente un move, y cómo leer el error de valor movido.
En casi todos los lenguajes, b = a deja a ambos apuntando al mismo objeto o hace una copia silenciosa. En Rust, para un valor que posee recursos, let b = a mueve la propiedad: b pasa a ser el dueño y a deja de existir a efectos del compilador. Este único comportamiento —el move por defecto— es la mecánica que hace cumplir la regla del dueño único.
- Entender qué significa que la propiedad se mueve al asignar o pasar un valor.
- Ver por qué el move invalida el origen: es lo que evita el doble free.
- Distinguir un move (copia superficial del stack) de un
clone(copia profunda). - Leer y corregir el error E0382 “borrow of moved value”.
Asignar mueve, no copia
Con un valor no-Copy como String, la asignación transfiere la propiedad. Solo se copian las tres palabras del stack —puntero, longitud y capacidad—; el buffer del heap no se toca. Y el origen queda invalidado:
let s1 = String::from("hola");
let s2 = s1; // MOVE: s1 cede la propiedad del buffer a s2
// println!("{s1}"); // ERROR E0382: s1 ya no es dueño de nada
println!("{s2}"); // s2 es el único dueño válido del buffer
El move es barato: mover un Vec de un millón de elementos copia solo esas tres palabras, nunca el millón de elementos. La propiedad del buffer viaja; los datos se quedan quietos en el heap.
Un tipo es “no-Copy” cuando no implementa el trait marcador Copy —típicamente porque gestiona un recurso en el heap, como String, Vec<T> o Box<T>—. Para esos tipos, la asignación mueve. Los tipos Copy (enteros, bool, char) se duplican en su lugar y el original sobrevive. Esa frontera es el tema de la siguiente lección; por ahora, quédate con que String mueve y i32 copia.
Por qué el move invalida el origen
Imagina que Rust no invalidara s1. Entonces s1 y s2 guardarían el mismo puntero al mismo buffer. Al final del scope, la regla 3 ejecutaría el drop de los dos, y cada uno intentaría liberar el mismo buffer: un doble free, el bug que corrompe el asignador y abre vulnerabilidades. Invalidar s1 es la forma que tiene Rust de mantener la regla del dueño único y, con ella, garantizar exactamente un drop por buffer.
flowchart TB s1[s1 posee el buffer] -->|al asignar a s2| copia[Se copian ptr len y cap] copia --> s2[s2 es el nuevo dueno del buffer] copia --> inval[s1 queda invalidado en compilacion]
Un move en Rust es siempre una copia trivial de bits (un memcpy del stack) seguida de la invalidación estática del origen. No existe un “constructor de movimiento” como en C++: ningún código de usuario corre durante un move, no se puede interceptar ni personalizar, y el compilador incluso puede eliminar la copia si demuestra que sobra. Por eso los moves son predecibles y gratis.
En la jerga clásica, copiar solo el puntero (sin el buffer) es una “copia superficial” y copiar también el buffer es “profunda”. El move de Rust es exactamente una copia superficial a la que se le añade la invalidación del origen. Ese pequeño extra —invalidar— es lo que la vuelve segura: una copia superficial sin invalidación es precisamente el bug de doble free que arrastran C y C++.
Move frente a clone
Si de verdad quieres dos dueños independientes, pídelo de forma explícita con clone, que sí hace una copia profunda: reserva un buffer nuevo en el heap y copia los bytes.
let s1 = String::from("hola");
let s2 = s1.clone(); // COPIA PROFUNDA: buffer nuevo en el heap
println!("{s1} y {s2}"); // ambos válidos, dueños de buffers distintos
La asimetría es deliberada. El move es el defecto silencioso y barato; el clone, potencialmente caro, es visible en el código. Rust no oculta un coste de duplicación: si ves .clone(), sabes que ahí hubo una asignación de memoria; si no lo ves, no la hubo.
En C++, copiar un objeto pesado puede ocurrir de forma implícita al pasarlo a una función, y ese coste queda escondido en la firma. En Rust es imposible: o mueves (gratis) o clonas (y el .clone() aparece negro sobre blanco en el sitio exacto). Al leer código Rust, cada asignación de heap es visible. Esa transparencia hace que los perfiles de rendimiento raras veces sorprendan.
Dónde ocurren los moves
El move no es exclusivo del signo =. Ocurre en cada punto donde un valor cambia de dueño, y todos disparan el mismo E0382 si luego usas el origen. Conviene reconocerlos:
fn tragar(_s: String) {}
fn main() {
let a = String::from("x");
tragar(a); // move al pasar por valor a una función
let b = String::from("y");
let mut v = Vec::new();
v.push(b); // move al insertar en un contenedor
let c = String::from("z");
let _d = match c { // move al hacer match por valor
s => s,
};
for palabra in vec![String::from("a"), String::from("b")] {
println!("{palabra}"); // cada iteración mueve un elemento del Vec
}
}
La regla mnemónica: si un valor no-Copy “entra” en algún sitio por valor —una función, un contenedor, una rama de match, una iteración for—, lo estás moviendo, y el nombre original deja de servir.
El error “borrow of moved value”
Este es el error que más vas a leer al empezar. El compilador señala tres cosas: dónde se movió el valor, dónde intentas volver a usarlo, y qué tipo era.
let original = String::from("dato");
let copia = original; // value moved here
println!("{original}"); // value borrowed here after move -> E0382
Si el segundo uso es una lectura por referencia (como en println!), el mensaje dice “borrow of moved value”; si lo usas por valor, dice “use of moved value”. La causa es la misma: intentas tocar un dueño que ya cedió su propiedad. Tres arreglos, según lo que quieras: prestar en vez de mover (&original, el borrowing del nivel 9), clonar si necesitas dos copias reales, o reordenar para no volver a usar el origen.
// 1. Prestar: la función lee sin apropiarse (métodos que toman &self no mueven).
let s = String::from("dato");
let n = s.len();
println!("{s} mide {n}"); // `s` sigue viva
// 2. Clonar: dos dueños independientes cuando de verdad necesitas dos copias.
let original = String::from("dato");
let copia = original.clone();
println!("{original} y {copia}");
// 3. Reordenar: usa el origen ANTES de moverlo, no después.
let dato = String::from("dato");
println!("{dato}"); // último uso del origen...
let dueno = dato; // ...ahora sí, muévelo sin conflicto
println!("{dueno}");
La elección entre los tres arreglos no es estética: prestar es lo más barato y casi siempre lo correcto; clonar, solo cuando de verdad necesitas dos copias vivas; reordenar, cuando el segundo uso del origen era en realidad innecesario. Aprender a elegir rápido es buena parte de lo que significa “domar el borrow checker”.
Puedes mover un solo campo de un struct, no solo el valor entero. Tras ese move parcial, el campo tiene nuevo dueño, pero el struct como un todo queda inutilizable: no puedes moverlo ni pasarlo completo mientras le falte una pieza, aunque sí puedes seguir leyendo los campos intactos. Un valor con un agujero no es un valor válido.
struct Par { a: String, b: String }
let p = Par { a: String::from("x"), b: String::from("y") };
let sacado = p.a; // move parcial: se lleva solo el campo `a`
println!("{}", p.b); // OK: `b` sigue en su sitio
// let q = p; // ERROR: no se puede mover `p` completo, le falta `a`
println!("{sacado}");
La lección profunda del move es que la propiedad no es una etiqueta abstracta: es la responsabilidad concreta de ejecutar el drop. Mover un valor es transferir esa responsabilidad, y por eso el origen tiene que morir: dos variables no pueden ser ambas responsables de liberar el mismo buffer sin provocar un doble free. Aquí Rust y C++ divergen en filosofía. En C++, un move deja el origen en un “estado válido pero no especificado” —sigue existiendo, sigue teniendo destructor, y usarlo es legal aunque casi siempre un bug—. En Rust, el origen simplemente deja de existir para el compilador: usarlo no es un bug silencioso, es un error que no compila. Esa diferencia —invalidación estática frente a estado fantasma— es la razón por la que los moves de Rust son seguros por construcción y no requieren que recuerdes ninguna convención. La máquina recuerda por ti.
Mover es lo que hace Rust por defecto con todo lo que posee heap: transfiere la propiedad copiando la parte del stack e invalidando el origen, en tiempo constante. Si el compilador te frena con un E0382, no está siendo pedante: te avisa de que intentas usar algo cuya responsabilidad ya cediste. Prestar, clonar o reordenar son tus tres salidas.
- Escribe
let a = String::from("x"); let b = a;y usaa: lee entero el error E0382 e identifica las etiquetas “moved here” y “borrowed here after move”. - Arregla el mismo código de tres maneras distintas: clonando, reordenando y prestando con
&. - Mete un
Stringen unVecconpushy comprueba que la variable original quedó movida. - Explica por qué mover un
Vecenorme cuesta lo mismo que mover uno vacío. - Provoca un move parcial sacando un campo
Stringde unstructy comprueba que ya no puedes mover elstructcompleto.