Swift y Objective-C

Copia superficial vs. copia profunda: hasta Swift COW

La copia por referencia, la copia superficial y la copia profunda difieren en el alcance de los datos que crean de nuevo. Explicamos las trampas de incluir tipos por referencia dentro de tipos por valor en Swift y cómo Copy-on-Write mantiene copias independientes.

4 min de lectura
Imagen de portada de Copia superficial vs. copia profunda: hasta Swift COW

Copié un array en una variable nueva. Modifiqué solo la copia, pero el original también cambió. Es un misterio con el que todo programador se encuentra al menos una vez.

El culpable son las dos caras de la palabra «copia». Una copia duplica el valor completo; la otra solo crea otra etiqueta que apunta a los mismos datos.

Aclararemos cómo distinguir una copia superficial de una profunda y cómo cambia el problema en Swift. Los tipos por valor y Copy-on-Write cambian la situación, pero las trampas persisten.

Este es el resumen clave.

Al aplicar esta distinción a los patrones de creación de objetos, se llega a Patrón Prototype de Swift y NSCopying; la implementación que retrasa la copia real por rendimiento se explica en detalle en Swift Copy-on-Write.

  1. Copia por referencia: solo se duplica la etiqueta que apunta a los datos. Si modificas un lado, cambia el otro
  2. Copia superficial: se crea una nueva envoltura, pero las referencias internas siguen apuntando a los mismos objetos
  3. Copia profunda: todo el contenido se recrea de forma recursiva. Las copias quedan completamente independientes
  4. Los tipos por valor de Swift se comportan como copias independientes al asignarse, pero si contienen una clase, reaparece el problema de la copia superficial

Hay que distinguir tres niveles

A menudo se habla de dos tipos, superficial y profunda, pero la imagen solo es correcta si incluimos la copia por referencia como un tercer nivel.

La copia por referencia no es una copia, sino asignar un alias. Al asignar una instancia de clase a otra variable, solo hay un objeto y dos referencias que lo señalan.

class Profile { var name = "Desarrollador Kim" }
let a = Profile()
let b = a          // Copia por referencia: un solo objeto
b.name = "Programador Lee"
print(a.name)      // "Programador Lee» — el original también cambia

La copia superficial crea una sola capa exterior nueva. Si el contenido son referencias, los objetos a los que apuntan se siguen compartiendo.

La copia profunda recrea incluso las cajas dentro de otras cajas, hasta el último nivel. Las dos estructuras son totalmente independientes y modificar una no afecta a la otra.

La copia superficial recrea solo la envoltura; la profunda recrea todo el contenido
La copia superficial recrea solo la envoltura; la profunda recrea todo el contenido

Tipos por valor de Swift: asignar equivale a copiar

Las estructuras, enumeraciones y tipos básicos de Swift son tipos por valor, por lo que el valor se duplica al asignarlo.

var original = [1, 2, 3]
var copy = original
copy.append(4)
print(original) // [1, 2, 3] — Seguro

La diferencia es grande frente a los lenguajes donde asignar un array copia la referencia. En Python, asignar una lista hace que ambas variables apunten a la misma lista.

«¿No será lento copiarlo todo cada vez?». Aquí aparece Copy-on-Write (COW). Array, Dictionary y String comparten el búfer interno al asignarse y solo realizan la copia real cuando uno de los lados se modifica. Leer no tiene coste de copia y, semánticamente, se comportan siempre como valores independientes. Es un diseño que combina la seguridad de los tipos por valor con la eficiencia de compartir referencias.


Pero las trampas persisten

En cuanto un tipo por valor contiene un tipo por referencia, revive el problema de la copia superficial.

class Attachment { var filename = "a.png" }

struct Mail {
    var title: String
    var attachment: Attachment  // Una clase dentro de una estructura
}

var mail1 = Mail(title: "Original", attachment: Attachment())
var mail2 = mail1               // La estructura se copia, pero...
mail2.attachment.filename = "b.png"
print(mail1.attachment.filename) // "b.png" — El original también cambia!

La estructura Mail sí se ha copiado. title es independiente. Sin embargo, attachment contenía una «referencia», así que lo copiado también es una referencia. Los dos Mail comparten el mismo Attachment. Esa es exactamente la definición de copia superficial.

Para resolverlo, debes crear una copia profunda manualmente. Puedes escribir una lógica que cree un Attachment nuevo o convertir Attachment en una estructura desde el principio. En la familia Objective-C, lo tradicional es implementar copy(with:) de NSCopying; si una clase contiene otra clase, también hay que copiarla recursivamente para obtener una copia profunda real.

Desde el punto de vista práctico, la conclusión coincide con el motivo por el que Swift recomienda los tipos por valor. Si un modelo está compuesto únicamente por estructuras y propiedades de tipo por valor, el problema desaparece.

Usar solo tipos por valor en el modelo elimina el problema
Usar solo tipos por valor en el modelo elimina el problema

En una frase para entrevistas

«Una copia superficial crea solo una nueva envoltura de nivel superior y comparte las referencias internas; una copia profunda duplica recursivamente el interior y la hace completamente independiente. Los tipos por valor de Swift se comportan como copias independientes al asignarse, pero sus propiedades de tipo por referencia siguen compartidas, así que hay que tener cuidado.»

Para las preguntas de seguimiento, basta con responder «¿Qué es COW?» (una optimización que retrasa la copia hasta la modificación) y «¿Cómo implementas una copia profunda?» (NSCopying o duplicación manual recursiva).


Resumen

  • Entiende la copia en tres niveles: copia por referencia → copia superficial → copia profunda
  • La copia por referencia duplica solo la etiqueta (objeto compartido); la superficial duplica solo la envoltura (referencias internas compartidas); la profunda duplica todo (independencia total)
  • En los tipos por valor de Swift, asignar equivale a copiar; Array y String retrasan la copia real hasta la modificación mediante COW
  • Si un tipo por valor contiene una propiedad de clase, el problema de la copia superficial se reproduce tal cual
  • Cuando necesitas una copia profunda, implementa NSCopying o una lógica de duplicación manual de forma recursiva
  • La solución fundamental es construir modelos con tipos por valor: otra razón por la que Swift recomienda las estructuras