Copy vs Move: cuándo se copia y cuándo se mueve
Los tipos que implementan `Copy` se duplican en vez de moverse. Por qué los datos que viven solo en el stack son `Copy` y los que gestionan el heap, como `String` o `Vec`, nunca pueden serlo.
En la lección anterior, let b = a invalidaba a. Pero con let b = a sobre un entero, a sigue perfectamente vivo. La diferencia no es magia ni un caso especial: es un trait marcador llamado Copy, y su presencia o ausencia decide si una asignación mueve o duplica. Entender qué tipos lo tienen, y por qué, es entender dónde está la frontera entre el stack y el heap.
- Saber qué tipos son
Copyy cuáles no, y por qué. - Entender que
Copysolo decide si el origen sobrevive a la asignación. - Ver por qué
CopyyDropson mutuamente excluyentes. - Distinguir
Copy(implícito y trivial) deClone(explícito y arbitrario).
Los tipos Copy no se mueven
Para un tipo Copy, la asignación y el paso por valor duplican los bits y dejan el original intacto. No hay move, no hay invalidación:
let a = 5;
let b = a; // COPIA: se duplican los bits de a
println!("{a} y {b}"); // ambos válidos: i32 es Copy
let p = (1, 2.0, true);
let q = p; // tuplas de tipos Copy también son Copy
println!("{p:?} y {q:?}");
Como la copia es implícita, un valor Copy sobrevive a cuantas llamadas quieras: pasarlo por valor lo duplica, nunca lo consume.
fn imprime(n: i32) { println!("{n}"); }
fn main() {
let x = 42;
imprime(x);
imprime(x); // válido: cada llamada copió x, no lo movió
println!("{x}");
}
Son Copy los tipos que caben enteros en el stack y no gestionan ningún recurso externo: todos los enteros (i32, u64, usize, …), los flotantes, bool, char, las referencias compartidas &T, y las tuplas y arrays [T; N] cuyos elementos sean todos Copy.
Las referencias compartidas también son Copy, lo que a veces sorprende: duplicar un &T produce dos punteros que apuntan al mismo dato sin poseerlo, y eso es inofensivo porque ninguno es responsable de liberar nada.
let s = String::from("dato");
let r1 = &s;
let r2 = r1; // COPIA de la referencia, no del String
println!("{r1} y {r2}"); // ambas referencias válidas a la vez
Por qué el stack es Copy y el heap no
Copy significa una cosa muy concreta: duplicar los bits produce dos valores independientes y ambos válidos. Para un i32 eso es trivialmente cierto: sus 4 bytes son el valor completo, y dos copias no comparten nada.
Un String rompe esa promesa. Sus bits en el stack son un puntero, una longitud y una capacidad. Duplicarlos daría dos triples que apuntan al mismo buffer del heap: dos dueños del mismo recurso, es decir, un doble free garantizado al final del scope. Por eso String, Vec<T>, Box<T> y cualquier tipo que posea algo en el heap no pueden ser Copy: para ellos, la asignación tiene que mover.
Copy · vive en el stack
i32, f64, bool, char, &T, tuplas y arrays de Copy. Duplicar los bits es seguro: no hay recurso compartido.
Move · gestiona el heap
String, Vec<T>, Box<T>, Rc<T>. Duplicar los bits daría dos dueños del mismo buffer: por eso se mueven.
Toda la decisión se reduce a una pregunta, y el compilador la responde por ti mirando la estructura del tipo:
flowchart TB q[El tipo posee un recurso en el heap] -->|si| mv[No puede ser Copy y se mueve] q -->|no| cp[Sus bits son el valor entero y puede ser Copy]
Copy y Drop se excluyen
De aquí sale una ley del sistema de tipos: un tipo no puede ser Copy y Drop a la vez. Es coherente. Drop significa “tengo una limpieza especial que hacer al morir” (liberar un buffer, cerrar un fichero). Copy significa “duplicarme es un memcpy sin consecuencias”. Un tipo que necesita limpieza no puede duplicarse libremente sin duplicar también esa limpieza. El compilador rechaza #[derive(Copy)] sobre cualquier tipo que implemente Drop.
#[derive(Copy, Clone)]
struct Punto { x: f64, y: f64 } // todos los campos son Copy → válido
// Esto NO compilaría: un campo String no es Copy, y arrastra un Drop.
// #[derive(Copy, Clone)]
// struct Etiqueta { texto: String }
Un tipo compuesto puede ser Copy solo si todos sus campos lo son. Añade un String y pierdes esa posibilidad de golpe: el tipo pasa a moverse, como sus componentes.
Si intentas #[derive(Copy, Clone)] sobre un struct con un campo String, el compilador responde con E0204: “the trait Copy may not be implemented for this type”, y señala el campo culpable. No es una restricción arbitraria: te recuerda que ese campo posee un recurso y que copiarlo en silencio rompería la propiedad única. Un tipo con Drop nunca puede ser Copy, y a la inversa.
Cuándo hacer Copy tus propios tipos
Que un tipo pueda ser Copy no significa que deba serlo. Copy encaja con valores pequeños, sin identidad, que se comportan como números: coordenadas, colores, banderas, identificadores. Se activa derivándolo, siempre junto a Clone (su supertrait):
#[derive(Copy, Clone, Debug)]
struct Color { r: u8, g: u8, b: u8 }
fn main() {
let negro = Color { r: 0, g: 0, b: 0 };
let otro = negro; // copia: `negro` sigue usable
println!("{negro:?} y {otro:?}");
}
Pero Copy tiene un filo oculto: vuelve las copias invisibles. Un [u8; 4096] es Copy, así que pasarlo por valor duplica cuatro kibibytes en silencio en cada llamada, sin un .clone() que lo delate. Para tipos grandes, dejar que se muevan —o prestarlos— suele ser mejor aunque técnicamente pudieran ser Copy. La pregunta de diseño no es solo “¿es seguro copiar estos bits?”, sino “¿quiero que copiarse sea tan barato de escribir que ocurra sin que me entere?”.
Una heurística que envejece bien: haz Copy los tipos diminutos que representan un valor matemático (un punto, un identificador numérico, un enum sin datos); deja que se muevan los tipos que poseen recursos; y para el resto, ni copies ni muevas de más: presta con &. Copiar lo trivial, mover lo propio, prestar lo compartido — ese es el ritmo del Rust idiomático.
Copy frente a Clone
Copy y Clone se confunden, pero tienen roles distintos. En la biblioteca estándar, Copy es un supertrait de Clone: su declaración es pub trait Copy: Clone {}, un marcador sin métodos. Implica que duplicar es un memcpy implícito, barato y siempre correcto. Clone, en cambio, se invoca explícitamente con .clone() y puede ejecutar código arbitrario: una copia profunda del heap, un incremento de contador de referencias, lo que el tipo necesite.
let s = String::from("hola");
let t = s.clone(); // Clone: explícito, puede ser caro (copia el buffer)
let n = 5;
let m = n; // Copy: implícito, siempre trivial
Un tipo puede ser Clone sin ser Copy —de hecho es lo habitual para los que gestionan heap—: derivas solo Clone y las duplicaciones quedan explícitas y controladas.
#[derive(Clone)]
struct Usuario { nombre: String, edad: u32 }
fn main() {
let a = Usuario { nombre: String::from("Ada"), edad: 36 };
let b = a.clone(); // copia profunda explícita; `a` sigue viva
println!("{} y {}", a.nombre, b.nombre);
}
El propio String es el ejemplo canónico: es Clone (por eso existe .clone()) pero jamás Copy. Un matiz revelador: la operación de máquina es la misma en un move y en una copia —un memcpy del stack—. Lo único que cambia Copy es si, después, el compilador invalida el origen o no. “Mover” y “copiar” se diferencian en el sistema de tipos, no necesariamente en las instrucciones generadas. Nota final: &T es Copy, pero &mut T no lo es; duplicar una referencia mutable rompería la exclusividad que Rust garantiza sobre ella.
El trait Copy parece una etiqueta trivial, pero codifica la decisión más importante sobre cualquier tipo: si su representación en bits es el valor completo, o si esos bits son solo un mango que apunta a un recurso vivo en otra parte. Si son el valor completo (el stack puro), duplicar es inofensivo y el original sobrevive. Si son un mango a un recurso (el heap), duplicar crea dos dueños de una cosa que solo uno puede liberar, y por eso hay que mover. La frontera stack/heap que aprendiste como detalle de rendimiento es, en realidad, la frontera de la propiedad: marca exactamente dónde Copy se vuelve imposible. Cuando diseñes tus propios tipos, la pregunta “¿puede ser Copy?” es en el fondo “¿este tipo posee algún recurso?”. Si la respuesta es sí, deja que se mueva; forzar una copia sería mentir sobre quién es responsable de liberarlo.
Tres frases para llevarte: (1) Copy duplica y deja vivo el original; su ausencia (move) invalida el original. (2) Solo puede ser Copy lo que no posee recursos y no implementa Drop. (3) Clone es la duplicación explícita y potencialmente cara; Copy es su caso trivial e implícito, reservado a datos que viven enteros en el stack.
- Comprueba con
let a = 5; let b = a; println!("{a}")que uni32sobrevive a la asignación, y contrasta con el mismo patrón sobre unString. - Define un
structde dosf64y hazloCopycon#[derive(Copy, Clone)]; luego añádele un campoStringy lee el error que aparece. - Explica por qué
&mut Tno puede serCopyapelando a la exclusividad. - Predice, para un
Vec<i32>, si esCopyo no, y justifica la respuesta con la regla del recurso en el heap. - Deriva
Clone(sinCopy) sobre unstructcon unStringy comprueba que ahora.clone()funciona pero la asignación sigue moviendo.