Swift e Objective-C

Cópia rasa vs. cópia profunda: até o COW do Swift

Cópia por referência, cópia rasa e cópia profunda diferem no alcance dos dados recriados. Este artigo explica as armadilhas de tipos por referência dentro de tipos por valor no Swift e como o Copy-on-Write mantém cópias independentes.

4 min de leitura
Imagem de capa de Cópia rasa vs. cópia profunda: até o COW do Swift

Copiei um array para uma nova variável. Alterei apenas a cópia, mas o original também mudou. É um mistério que todo programador encontra pelo menos uma vez.

O culpado são as duas faces da palavra “cópia”. Uma cópia duplica o valor inteiro; a outra apenas cria mais um rótulo apontando para os mesmos dados.

Vamos esclarecer a diferença entre cópia rasa e profunda e ver como o Swift muda esse cenário. Tipos por valor e Copy-on-Write ajudam, mas as armadilhas continuam.

Este é o resumo principal.

Aplicar essa distinção a padrões de criação de objetos leva a Padrão Prototype do Swift e NSCopying; já a implementação que adia a cópia real por desempenho é detalhada em Swift Copy-on-Write.

  1. Cópia por referência: apenas o rótulo que aponta para os dados é duplicado. Alterar um lado altera o outro
  2. Cópia rasa: a camada externa é nova, mas as referências internas continuam apontando para os mesmos objetos
  3. Cópia profunda: todo o conteúdo é recriado recursivamente. As cópias ficam totalmente independentes
  4. Tipos por valor do Swift se comportam como cópias independentes na atribuição, mas, se contiverem uma classe, o problema da cópia rasa volta a aparecer

É preciso separar três níveis

É comum falar apenas em cópia rasa e profunda, mas o quadro só fica correto quando a cópia por referência é incluída como um terceiro nível.

Cópia por referência não é cópia; é dar um alias. Ao atribuir uma instância de classe a outra variável, existe um único objeto e duas referências apontando para ele.

class Profile { var name = "Desenvolvedor Kim" }
let a = Profile()
let b = a          // Cópia por referência: um objeto
b.name = "Programador Lee"
print(a.name)      // "Programador Lee” — o original também muda

Cópia rasa cria apenas uma nova camada externa. Se o conteúdo for composto por referências, os objetos apontados continuam compartilhados.

Cópia profunda recria a caixa, as caixas dentro dela e tudo que estiver abaixo, até o fim. As duas estruturas são totalmente independentes; alterar uma não afeta a outra.

A cópia rasa recria apenas a camada externa; a profunda recria todo o conteúdo
A cópia rasa recria apenas a camada externa; a profunda recria todo o conteúdo

Tipos por valor no Swift: atribuição é cópia

Structs, enums e tipos básicos do Swift são tipos por valor; por isso, o valor é duplicado no momento da atribuição.

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

Isso é bem diferente de linguagens em que atribuir um array faz uma cópia por referência. No Python, atribuir uma lista faz com que as duas variáveis apontem para a mesma lista.

“Copiar tudo sempre não seria lento?” É aqui que entra o Copy-on-Write (COW). Array, Dictionary e String compartilham o buffer interno no momento da atribuição e só fazem a cópia real quando um dos lados é alterado. Ler não tem custo de cópia e, semanticamente, os valores sempre se comportam como independentes. É um design que combina a segurança dos tipos por valor com a eficiência do compartilhamento de referências.


Mas as armadilhas continuam

Assim que um tipo por valor contém um tipo por referência, o problema da cópia rasa reaparece.

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

struct Mail {
    var title: String
    var attachment: Attachment  // Uma classe dentro de uma struct
}

var mail1 = Mail(title: "Original", attachment: Attachment())
var mail2 = mail1               // A struct é copiada, mas...
mail2.attachment.filename = "b.png"
print(mail1.attachment.filename) // "b.png" — O original também muda!

A struct Mail foi copiada de fato. title é independente. Porém, attachment continha uma “referência”, então o que foi copiado também é uma referência. Os dois Mail passam a compartilhar o mesmo Attachment. Essa é exatamente a definição de cópia rasa.

Para resolver isso, é preciso criar uma cópia profunda manualmente. Você pode escrever uma lógica que crie um novo Attachment ou transformar Attachment em uma struct desde o início. Na família Objective-C, a abordagem tradicional é implementar copy(with:) de NSCopying; se uma classe contiver outra classe, ela também deverá ser copiada recursivamente para obter uma cópia profunda de verdade.

Na prática, a conclusão coincide com o motivo pelo qual o Swift recomenda tipos por valor. Se um modelo for composto apenas por structs e propriedades de tipos por valor, o problema desaparece.

Usar apenas tipos por valor no modelo elimina o problema
Usar apenas tipos por valor no modelo elimina o problema

Em uma frase para entrevistas

“A cópia rasa cria apenas uma nova camada externa de nível superior e compartilha as referências internas; a cópia profunda duplica recursivamente o conteúdo interno e torna a cópia totalmente independente. Tipos por valor do Swift se comportam como cópias independentes na atribuição, mas propriedades de tipos por referência dentro deles continuam compartilhadas, então é preciso ter cuidado.”

Para perguntas complementares, basta responder “O que é COW?” (uma otimização que adia a cópia até a alteração) e “Como implementar uma cópia profunda?” (NSCopying ou duplicação manual recursiva).


Resumo

  • Entenda a cópia em três níveis: cópia por referência → cópia rasa → cópia profunda
  • A cópia por referência duplica apenas o rótulo (objeto compartilhado); a cópia rasa duplica apenas a camada externa (referências internas compartilhadas); a cópia profunda duplica tudo (totalmente independente)
  • Nos tipos por valor do Swift, atribuição é cópia; Array e String adiam a cópia real até a alteração usando COW
  • Se um tipo por valor tiver uma propriedade de classe, o problema da cópia rasa será reproduzido integralmente
  • Quando uma cópia profunda for necessária, implemente NSCopying ou uma lógica manual de duplicação recursiva
  • A solução fundamental é construir modelos com tipos por valor — outro motivo para o Swift recomendar structs