Clone y Copy: la copia profunda y la duplicación de bits
Clone es la copia profunda explícita que pides con punto clone; Copy es la duplicación implícita de tipos triviales del stack. Por qué no todo puede ser Copy, y la idea que casi nadie ve: mover y copiar son la misma copia de bits, solo cambia el destino del original.
Ya sabes que asignar un String lo mueve e invalida el original, mientras que asignar un i32 deja los dos válidos. Detrás de esa diferencia hay dos traits: Clone, la copia profunda que pides a mano, y Copy, la duplicación implícita que Rust hace por ti para los tipos triviales. Parecen dos formas de lo mismo, pero encierran una de las ideas más finas del lenguaje: mover y copiar son, para la máquina, la misma operación; lo único que cambia es qué le pasa al original después.
- Usar
Clonepara producir una copia profunda e independiente con.clone(). - Entender
Copycomo duplicación implícita de bits para tipos del stack. - Explicar por qué un tipo con
StringoVecnunca puede serCopy. - Ver que
moveyCopyson la misma copia de bits con distinto destino del origen.
Clone: la copia profunda explícita
Clone añade el método .clone(), que produce un duplicado completamente independiente, incluida la memoria del heap que el valor posea.
#[derive(Debug, Clone)]
struct Perfil {
nombre: String,
etiquetas: Vec<String>,
}
fn main() {
let a = Perfil { nombre: String::from("Ada"), etiquetas: vec![String::from("phd")] };
let b = a.clone(); // copia PROFUNDA: reserva nuevo heap para el String y el Vec
// 'a' sigue siendo válido; 'a' y 'b' no comparten ni un byte del heap
println!("{a:?} / {b:?}");
}
Que Clone sea explícito es intencional. Clonar un String o un Vec reserva memoria y copia su contenido: es un coste real, potencialmente grande. Rust te obliga a escribir .clone() para que ese coste sea visible en el código; nunca ocurre a tus espaldas. El derive genera un impl que clona campo a campo, así que la capacidad viaja recursivamente: Perfil puede derivar Clone porque String y Vec<String> ya lo son.
.clone() es una herramienta legítima, pero cuando aparece solo para esquivar un error de préstamo suele delatar un diseño mejorable: estás pagando una copia profunda por no decidir quién posee el dato. Antes de clonar, pregúntate si bastaría con prestar (&T) o mover. Hay una excepción idiomática que conviene conocer ya: clonar un Rc<T> o un Arc<T> no duplica el dato, solo incrementa un contador de referencias —es baratísimo y perfectamente idiomático—. No todo .clone() es caro; lo caro es clonar lo que posee heap, como un String o un Vec grande.
El trait Clone no expone un método, sino dos: clone, que ya conoces, y clone_from(&mut self, fuente: &Self), que sobrescribe self con una copia de fuente. Este segundo trae implementación por defecto, pero puede reutilizar la memoria ya reservada en self en lugar de liberarla y volver a pedirla. Para un Vec grande que actualizas en un bucle, destino.clone_from(&origen) ahorra reasignaciones que destino = origen.clone() provocaría. Rara vez lo escribes a mano, pero explica por qué Clone es más rico de lo que aparenta.
Copy: la duplicación implícita del stack
Copy es un trait marcador: no añade ningún método. Lo que hace es cambiar la semántica del lenguaje para ese tipo. Con Copy, la asignación deja de mover y pasa a duplicar los bits, y el original sigue vivo.
#[derive(Debug, Clone, Copy)]
struct Punto {
x: i32,
y: i32,
}
fn main() {
let p = Punto { x: 1, y: 2 };
let q = p; // COPIA implícita de bits; 'p' NO se invalida
println!("{p:?} {q:?}"); // ambos validos
}
Los tipos Copy son los triviales: enteros, f64, bool, char, y structs cuyos campos son todos Copy. Duplicarlos es copiar unos pocos bytes del stack, sin reservar memoria ni ejecutar ninguna lógica. Por eso Rust puede hacerlo implícitamente sin sorpresas de rendimiento: no hay coste oculto que esconder.
Nota que Copy requiere Clone (Copy: Clone): todo tipo Copy es también Clone, y su .clone() es simplemente esa copia de bits. Por eso los ves derivados juntos.
Por qué no todo puede ser Copy
Aquí está el límite duro. Un tipo puede ser Copy solo si todos sus campos lo son, y jamás si posee un recurso del heap. La razón no es una regla arbitraria, sino la seguridad de memoria.
Imagina que String fuera Copy. Entonces let b = a; copiaría los bits del String: el puntero al heap, la longitud y la capacidad. Ahora a y b contendrían el mismo puntero al mismo búfer, y ambos seguirían válidos. Cuando cada uno se destruyera al final del scope, cada uno liberaría ese búfer: un doble free, corrupción de memoria clásica. Copy significa exactamente “duplicar este valor es copiar sus bits, sin ninguna lógica de propiedad”; un tipo que posee memoria no cumple esa promesa, y por eso el lenguaje se lo prohíbe.
Un tipo Copy no puede implementar Drop, y viceversa. La razón es la misma moneda vista por la otra cara: Copy promete que el valor es libremente duplicable y trivialmente descartable —son solo bits—, mientras que Drop significa que hay un destructor con lógica de limpieza que ejecutar. Si un tipo necesita limpieza al morir, duplicar sus bits duplicaría también esa limpieza pendiente. El compilador rechaza cualquier tipo que intente ser las dos cosas.
La revelación: move y Copy son la misma copia de bits
Casi todo el mundo cree que mover y copiar son operaciones distintas a nivel de máquina. No lo son. Tanto let b = a; cuando a es un String (move) como cuando a es un Punto (Copy) hacen exactamente lo mismo en el procesador: un memcpy de los bytes de a a b. Para un String, esos bytes son el puntero, la longitud y la capacidad —el heap no se toca—; para un Punto, son sus dos enteros.
La única diferencia entre move y Copy es qué le pasa al origen después de la copia de bits:
Move (sin Copy)
Se copian los bits al destino y el compilador invalida el origen: a deja de poder usarse. Un solo dueño en todo momento.
Copy
Se copian los mismos bits al destino y el origen sigue válido: a y b conviven. Dos valores independientes, porque los bits no comparten heap.
flowchart TD A[let b igual a a] --> C[memcpy de los bits de a hacia b] C --> Q[El tipo es Copy] Q -->|no| M[Move: el origen a queda invalidado] Q -->|si| K[Copy: el origen a sigue valido] M --> S[Un unico dueno de la memoria] K --> D[Dos valores independientes sin heap compartido] style M fill:#f38ba8,color:#11111b style K fill:#a6e3a1,color:#11111b style S fill:#89b4fa,color:#11111b style D fill:#cba6f7,color:#11111b
Por eso Copy solo es seguro para tipos sin heap: si los bits copiados incluyeran un puntero a memoria poseída, dejar vivo el origen crearía dos dueños del mismo búfer. Copy no es “una copia más barata que Clone”; es la afirmación de que dejar vivo el original tras la copia de bits es seguro.
La intuición que hay que demoler es que move, Copy y Clone son tres operaciones de copia de distinto coste. En realidad hay una sola operación física —copiar bytes— y lo que Rust gestiona por encima de ella es la propiedad, que no es un dato sino un permiso. Cuando mueves un String, la máquina copia tres palabras (puntero, longitud, capacidad) exactamente igual que copiaría un Punto; lo que el compilador hace de más es revocar al origen el permiso de liberar ese heap y otorgárselo al destino, para que solo uno de los dos ejecute el free. Copy es la declaración de que un tipo no tiene ningún permiso que transferir —no posee nada que liberar—, y por eso el origen puede conservar sus bits sin peligro: dos copias de un entero no se pisan porque no hay nada compartido detrás. Clone, en el otro extremo, es lo que haces cuando quieres dos dueños de verdad: no basta con copiar el puntero, hay que reservar un heap nuevo y duplicar el contenido, y por eso es explícito y potencialmente caro. Vistos así, los tres no son grados de una escala de coste, sino tres respuestas distintas a una única pregunta: tras esta copia de bits, ¿quién tiene derecho a liberar la memoria? En el move, el destino; en Copy, nadie porque no hay memoria que liberar; en Clone, ambos, porque has fabricado una segunda memoria. Toda la gestión de recursos de Rust —y la ausencia de dobles free— cabe en esa pregunta.
- Define
struct Etiqueta { texto: String }y comprueba que no puedes derivarCopy. Lee el error y nómbralo con la palabra “propiedad”. - Cambia el campo a
id: u32y ahora sí derivaCopy. Asigna una instancia a otra variable y usa ambas: confirma que ninguna se invalida. - Con el struct
Copy, escribe una función que reciba el valor por parámetro y luego úsalo tras la llamada. ¿Por qué compila, si “se movió” a la función? - Añade un
impl Dropal structCopyy observa que deja de compilar. Explica por quéCopyyDropno pueden coexistir. - Escribe con tus palabras la única diferencia entre un move y un
Copya nivel de máquina, y qué gestiona el compilador por encima delmemcpy.