clone con criterio: cuándo copiar es la respuesta y cuándo es un síntoma
Clonar no es hacer trampa, pero tampoco es gratis ni neutral. Cómo distinguir un clone barato de uno caro, cuándo duplicar es la decisión correcta de diseño y cuándo delata un modelo de ownership mal planteado, y la mentalidad que convierte el clone en una elección consciente en vez de un reflejo defensivo.
Existe un consejo tóxico que circula entre quienes empiezan con Rust: “si el borrow checker te frena, pon un .clone()”. A veces es exactamente lo correcto; muchas otras, es tapar un problema de diseño con una asignación de heap que arrastrarás para siempre. clone no es hacer trampa ni es una derrota: es una herramienta con un coste concreto y un significado concreto. Dominar el borrow checker culmina en saber, ante cada .clone(), si estás resolviendo el problema o solo silenciando al mensajero.
- Distinguir un clone barato —
Rc,Arc, tiposCopypequeños— de uno caro que asigna heap. - Reconocer los casos donde clonar es la decisión de diseño correcta y de coste despreciable.
- Detectar el clone que tapa un ownership mal modelado y encontrar la cura estructural.
- Adoptar la mentalidad de ownership: modela dónde vive cada dato, no esquives la pregunta.
No todos los clone cuestan lo mismo
El primer error es tratar clone como una operación uniforme. Su coste varía en órdenes de magnitud según el tipo, y sin esa intuición no puedes decidir nada. Clonar un Rc<T> incrementa un contador y copia un puntero: nanosegundos, sin tocar el heap. Clonar un String de un megabyte asigna un megabyte nuevo y lo copia byte a byte. Son la misma llamada sintáctica y operaciones radicalmente distintas.
Clones baratos
Rc::clone y Arc::clone copian un puntero y ajustan un contador. Los tipos Copy (u32, bool, char) ni siquiera invocan Clone. Un String corto es una asignación pequeña y aislada.
Clones caros
Un Vec o String grande copia todo su buffer del heap. Un HashMap copia cada entrada. Hacerlo dentro de un bucle multiplica el coste por cada iteración: ahí un clone descuidado hunde el rendimiento.
Para el caso de compartir propiedad, la biblioteca estándar da la herramienta pensada para clonarse barato: Rc<T> (un hilo) y Arc<T> (varios hilos). Clonarlos no duplica el dato, sino la co-propiedad de un dato único:
use std::rc::Rc;
fn main() {
let compartido = Rc::new(vec![1, 2, 3, 4]); // un buffer, en el heap
let a = Rc::clone(&compartido); // +1 al contador, mismo buffer
let b = Rc::clone(&compartido); // +1 al contador, mismo buffer
println!("{} dueños del mismo dato", Rc::strong_count(&compartido)); // 3
println!("{:?} {:?}", a, b);
}
Para los punteros de conteo de referencias, prefiere la forma explícita Rc::clone(&x) sobre x.clone(). Ambas hacen lo mismo, pero la primera grita en el sitio de la llamada “esto es un clon barato de puntero, no una copia profunda del contenido”. Es una convención idiomática que salva a quien lee el código —incluido tu yo futuro— de confundir un incremento de contador con una duplicación de datos.
Cuándo clonar es la respuesta correcta
Clonar es legítimo y, con frecuencia, lo más sensato. Estos son los casos donde no debe darte ningún reparo:
- Cuando el coste es despreciable frente al trabajo real. Clonar una clave
Stringde veinte caracteres para insertarla en unHashMapmientras procesas millones de registros no aparecerá jamás en un perfilador. Optimizar ese clon es malgastar tu atención. - Cuando de verdad necesitas dos dueños independientes. Si dos partes del programa deben poseer y evolucionar su propia versión de un dato, clonar no esquiva el diseño: es el diseño. Duplicar es la semántica correcta.
- Cuando compartes propiedad por diseño.
Rc/Arcclonados son la manera idiomática de que varias partes co-posean un dato inmutable sin pelear con lifetimes. Aquí el clon es la arquitectura, no un parche. - En los límites de una API o para desbloquear con intención. Al cruzar una frontera —enviar a otro hilo, guardar en una estructura de vida larga— a menudo necesitas una copia propia. Y en un prototipo, un
clonepara avanzar y volver luego es una decisión válida, siempre que sea consciente.
use std::collections::HashMap;
fn indexar(registros: &[(String, u64)]) -> HashMap<String, u64> {
let mut mapa = HashMap::new();
for (nombre, valor) in registros {
mapa.insert(nombre.clone(), *valor); // clon barato de la clave: correcto y claro
}
mapa
}
Y cuando la co-propiedad debe cruzar hilos, Arc<T> —la variante atómica de Rc— es la herramienta correcta, no un clon profundo del dato:
use std::sync::Arc;
use std::thread;
fn main() {
let compartido = Arc::new(vec![1, 2, 3]);
let mut hilos = vec![];
for id in 0..3 {
let copia = Arc::clone(&compartido); // clon barato: cada hilo co-posee el mismo Vec
hilos.push(thread::spawn(move || {
println!("hilo {id} ve {copia:?}");
}));
}
for h in hilos {
h.join().unwrap();
}
}
Aquí clonar es la arquitectura misma de la concurrencia sin miedo: cada hilo recibe su Arc, todos comparten un único buffer inmutable, y el dato se libera cuando el último hilo suelta su copia. Duplicar el Vec entero para cada hilo sería tan innecesario como costoso.
Cuándo el clone es un síntoma
El mismo .clone() que arriba era sensato, en otro contexto delata un ownership mal planteado. Las señales de alarma son reconocibles:
// SÍNTOMA: clonar un buffer grande en cada iteración solo para leerlo.
fn total_mal(datos: &Vec<Vec<i32>>) -> i32 {
let mut suma = 0;
for fila in datos.clone() { // clona TODO el Vec de Vecs en cada llamada
suma += fila.iter().sum::<i32>();
}
suma
}
// CURA: prestar en vez de duplicar. Cero asignaciones.
fn total_bien(datos: &[Vec<i32>]) -> i32 {
let mut suma = 0;
for fila in datos { // itera por referencia: solo mira
suma += fila.iter().sum::<i32>();
}
suma
}
El clon del primero no expresa ninguna necesidad de propiedad: solo existe para que el bucle deje de quejarse, al precio de duplicar toda la estructura. Es la firma de tres antipatrones: clonar para “callar” un E0382 o E0502 que un & habría resuelto; clonar dentro de un bucle lo que podría izarse fuera o prestarse; y clonar para eludir un lifetime que un refactor mínimo satisfaría. La pregunta diagnóstica es siempre la misma: ¿este clon existe porque necesito otro dueño, o porque no quise pensar en el préstamo? Si es lo segundo, la cura vive en la lección anterior —prestar, indexar, reestructurar, secuenciar— y casi siempre deja el código más rápido y más claro.
Poner .clone() para que algo compile hoy es una decisión de deuda técnica perfectamente válida —a veces necesitas avanzar—, pero solo si la registras como deuda y vuelves. El peligro es que el clon funciona: el código compila, los tests pasan, y esa asignación silenciosa se queda para siempre, multiplicándose por cada dato que fluye por esa ruta. La deuda que nunca duele es la que nunca se paga. Marca cada clon de conveniencia y revísalo antes de dar por terminado el trabajo.
flowchart TB q[Voy a escribir un clone aqui] --> a[El clon es barato Rc Arc Copy o dato pequeno] a -->|si| ok[Clona sin culpa] a -->|no| b[Necesito de verdad un segundo dueno del dato] b -->|si| ok2[Correcto es el diseno no un parche] b -->|no| c[Solo intento callar un E0382 o un E0502] c -->|si| fix[Presta indexa o reestructura en su lugar]
La mentalidad de ownership
Todo este nivel converge aquí. El ownership no es un peaje que Rust cobra por la seguridad; es un modelo explícito de dónde vive cada dato, quién es responsable de liberarlo y quién puede tocarlo mientras vive. Esas preguntas existen en cualquier programa de sistemas —en C las contestas con comentarios, convenciones y esperanza—. Rust las eleva a tipos que el compilador verifica. Desde esa óptica, clone deja de ser “la tecla de escape del borrow checker” y se convierte en una afirmación de diseño precisa: “necesito que este dato exista de forma independiente en dos lugares”. Cuando esa afirmación es verdadera, clona sin dudar. Cuando es falsa, el clon es una mentira que el compilador aceptó pero que tu programa pagará.
Para los casos intermedios, Rust ofrece herramientas más finas que el binario prestar-o-clonar. Cow<str> (clone on write) pospone el clon hasta el instante en que de verdad necesitas mutar, y no antes:
use std::borrow::Cow;
// Solo asigna memoria si hay algo que corregir; si no, presta la entrada intacta.
fn normalizar(entrada: &str) -> Cow<str> {
if entrada.contains(' ') {
Cow::Owned(entrada.replace(' ', "_")) // clona solo cuando hace falta
} else {
Cow::Borrowed(entrada) // el caso común: cero asignaciones
}
}
Aquí está la síntesis de haber domado el borrow checker. Un programador novato clona a la defensiva: el compilador gruñe, aparece un .clone(), el gruñido cesa, y nunca se pregunta qué acaba de comprar. Un programador que ha interiorizado el ownership clona a la ofensiva: sabe exactamente qué cuesta cada clon, sabe qué afirmación de propiedad está haciendo, y elige clonar porque duplicar es la semántica correcta o porque el coste es irrelevante y su atención vale más en otra parte. La diferencia no está en clonar más o menos —los expertos clonan constantemente— sino en que cada clon del experto es una decisión medida y cada clon del novato es una rendición. Y hay una verdad más honda debajo: cada .clone() es una pequeña confesión de que, en ese punto, no expresaste la estructura de compartición mediante referencias, préstamos o co-propiedad, sino duplicando el dato. A veces esa confesión es sabia —la estructura compartida habría costado más en complejidad de lo que ahorra en memoria—. A veces delata que aún no ves cómo debería fluir la propiedad en tu programa. El maestro distingue las dos al instante, porque para él el borrow checker dejó hace tiempo de ser un obstáculo y se convirtió en lo que siempre fue: un modelo riguroso de la vida de los datos, el mismo que en otros lenguajes llevabas en la cabeza —mal, y a las tres de la madrugada—. Domar el borrow checker nunca fue vencerlo. Fue empezar a pensar como él.
clone no es trampa ni derrota: es una herramienta con coste y significado. Distingue el clon barato (Rc/Arc, Copy, datos pequeños) del caro (buffers grandes, sobre todo en bucles). Clona sin reparo cuando el coste es despreciable, cuando necesitas dos dueños de verdad, o cuando compartes propiedad con Rc/Arc. Sospecha del clon que solo existe para callar un E0382 o E0502: ahí la cura es prestar, indexar o reestructurar, y suele dejar el código mejor. Para el término medio, Cow pospone el clon hasta que muta. Y por encima de todo: trata cada clon como una afirmación consciente sobre dónde debe vivir tu dato.
- Mide mentalmente el coste de
Rc::clonede unVecgrande frente a.clone()de ese mismoVec, y explica por qué difieren en órdenes de magnitud. - Toma la función
total_malcondatos.clone()en el bucle y conviértela a la versión que presta; razona qué afirmación de propiedad era falsa. - Escribe un caso donde clonar sí sea correcto porque necesitas dos dueños independientes, y defiéndelo.
- Reescribe una función que clona su entrada
Stringincondicionalmente para que devuelvaCow<str>y solo clone al mutar. - Revisa un archivo propio, marca cada
.clone()y clasifícalo en decisión consciente o deuda; corrige al menos uno de los segundos.