rust-move-vs-copy-clone

Move vs Copy en Rust: transferencia y copia de datos

  • 4 min

Move y Copy son dos comportamientos al transferir valores mediante una asignación o una llamada.

En el artículo anterior soltamos una bomba: “Si asignas un String a otra variable, la original deja de funcionar”.

Esto choca frontalmente con la intuición que traemos de otros lenguajes.

  • Si hago a = b, espero que a y b valgan lo mismo y sean usables.
  • En Rust, dependiendo del tipo de dato, esto puede ser una Copia (Copy) o un Movimiento (Move).

Entender esta distinción es importante para dejar de pelearse con el compilador. Hoy vamos a ver qué ocurre bajo el capó 👇.

El comportamiento por defecto: move

Vamos a analizar el caso de los tipos complejos que usan el Heap, como String.

let s1 = String::from("hola");
let s2 = s1; // <-- Aquí ocurre el Move
Copied!

Conceptualmente, la representación de un String en Rust contiene tres partes:

  1. Un puntero a la memoria en el Heap (donde están las letras ‘h’, ‘o’, ‘l’, ‘a’).
  2. La longitud (cuánta memoria estamos usando).
  3. La capacidad (cuánta memoria hemos reservado).

Cuando hacemos let s2 = s1, Rust transfiere esa representación a s2. NO copia los datos del Heap.

Si Rust se detuviera ahí, tendríamos lo que en otros lenguajes se llama Shallow Copy (copia superficial). Tendríamos dos variables (s1 y s2) apuntando al mismo lugar en el Heap.

Recordemos la idea central del Ownership: solo un dueño a la vez. Si Rust permitiera que s1 y s2 existieran a la vez, al salir del ámbito ambas intentarían borrar la misma memoria (Double Free error).

Para evitar esto, Rust hace algo drástico: marca s1 como inválida.

Esto es un Move. Los datos no se han movido físicamente en la RAM (siguen en la misma dirección del Heap), pero la propiedad ha cambiado de manos.

let s1 = String::from("hola");
let s2 = s1;

println!("{}, world!", s1); // ❌ Error: value borrowed here after move
Copied!

Imagina que tienes las escrituras de una casa. Si le das las escrituras a tu hermano, la casa no se ha movido de sitio, pero tú ya no eres el dueño. No puedes entrar. Ahora el dueño es él.

Cuando necesitamos duplicar: Clone

¿Y si realmente queremos tener dos copias independientes de los datos? Es decir, queremos copiar la estructura del Stack Y TAMBIÉN los datos del Heap.

En el caso de String, esto implica una copia profunda y no ocurre automáticamente porque requiere reservar memoria. Si quieres hacerlo, debes pedirlo explícitamente usando el método .clone().

let s1 = String::from("hola");
let s2 = s1.clone(); // <-- Copia profunda explícita

println!("s1 = {}, s2 = {}", s1, s2); // ✅ Funciona
Copied!

Al ver .clone() sabemos que el tipo ha definido una duplicación explícita. En String copia el contenido, aunque no todos los clone son igual de costosos (clonar un Rc<T>, por ejemplo, solo incrementa un contador).

La excepción a la regla: tipos Copy

Ahora volvemos al caso que nos confundía. ¿Por qué esto funciona?

let x = 5;
let y = x;

println!("x = {}, y = {}", x, y); // ✅ Funciona perfectamente
Copied!

¿Por qué x no se ha invalidado? ¿Acaso los enteros no tienen dueño?

Copiar un entero solo requiere duplicar unos pocos bits. Además, no tiene una lógica de destrucción especial que pudiera ejecutarse dos veces.

Rust tiene un rasgo (trait) especial llamado Copy.

  • Si un tipo implementa Copy, al asignarlo se duplica en lugar de moverse.
  • La variable original sigue siendo válida.

Cómo saber qué ocurre

Cuando veas una asignación let a = b; o pases una variable a una función funcion(b);, pregúntate:

  1. ¿El tipo implementa Copy? La asignación duplica el valor y ambos nombres siguen siendo válidos.
  2. ¿No implementa Copy? La asignación mueve el valor y el nombre anterior deja de poder usarse.
  3. ¿Necesitas dos valores independientes? Comprueba si el tipo implementa Clone y valora el coste de llamar a .clone().