Featured image of post Cópia Rasa (Shallow Copy) vs. Cópia Profunda (Deep Copy): Uma Explicação em Nível de Memória

Cópia Rasa (Shallow Copy) vs. Cópia Profunda (Deep Copy): Uma Explicação em Nível de Memória

Compreender como os dados são copiados na programação é fundamental para escrever software robusto e previsível. A distinção entre cópia rasa (shallow copy) e cópia profunda (deep copy) é frequentemente uma fonte de confusão, mas é crítica para gerenciar a memória, prevenir efeitos colaterais não intencionais e garantir a integridade dos dados. Este artigo aprofunda as complexidades desses mecanismos de cópia, explorando suas diferenças conceituais, implementações específicas de linguagem e a dinâmica de memória subjacente que governa seu comportamento.

Introdução aos Mecanismos de Cópia

Quando um objeto é copiado, a operação essencialmente cria uma nova entidade que, em graus variados, se assemelha ao original. A natureza dessa semelhança — se é uma mera replicação de referências ou uma duplicação completa de todos os dados aninhados — define o tipo de cópia realizada. Essa distinção torna-se particularmente significativa ao lidar com estruturas de dados complexas que contêm outros objetos ou coleções.

Diferenças Conceituais: Rasa vs. Profunda

Cópia Rasa (Shallow Copy)

Uma cópia rasa cria um novo objeto, mas em vez de duplicar objetos aninhados, ela copia suas referências. Isso significa que o novo objeto terá seu próprio endereço de memória distinto, mas quaisquer objetos mutáveis aninhados dentro dele ainda apontarão para os mesmos locais de memória que os do objeto original. Consequentemente, modificações em objetos mutáveis aninhados, seja na estrutura original ou na copiada, serão refletidas em ambas.

Cópia Profunda (Deep Copy)

Uma cópia profunda, em contraste, cria um novo objeto e duplica recursivamente todos os objetos aninhados. Isso resulta em uma cópia completamente independente, onde não existem referências compartilhadas entre o objeto original e o novo objeto. Alterações feitas na cópia profunda, ou em seus componentes aninhados, não afetarão o objeto original, e vice-versa.

Dinâmica da Memória: Stack vs. Heap e Referências

Para compreender totalmente as cópias rasas e profundas, é essencial entender como a memória é gerenciada. A stack (pilha) é usada para alocação de memória estática, principalmente para tipos de dados primitivos e quadros de chamadas de função. A heap (monte) é usada para alocação de memória dinâmica, onde objetos e estruturas de dados complexas residem. Variáveis frequentemente armazenam referências (endereços de memória) para objetos na heap.

Semântica de Ponteiro/Referência

Em muitas linguagens, as variáveis não contêm diretamente objetos complexos, mas sim referências ou ponteiros para seus locais na memória. Quando ocorre uma cópia rasa, são essas referências que são duplicadas, não os objetos para os quais elas apontam. Uma cópia profunda, no entanto, desreferencia esses ponteiros e cria novos objetos em novos locais de memória para cada componente aninhado.

Mutabilidade vs. Imutabilidade

Mutabilidade refere-se à capacidade de um objeto ser alterado após sua criação. Objetos como listas, dicionários e instâncias de classes personalizadas são tipicamente mutáveis. Imutabilidade significa que um objeto não pode ser alterado após a criação (por exemplo, strings, números, tuplas em Python). As implicações das cópias rasas e profundas são mais pronunciadas com objetos mutáveis aninhados, pois as alterações nos componentes mutáveis compartilhados podem levar a um comportamento inesperado.

Visualizando a Memória: Diagramas ASCII

Considere um objeto A contendo um objeto aninhado mutável B.

Objeto Original

Stack:         Heap:
+---+          +-------------------+
| A | -------> | Objeto A          |
+---+          |   +-------------+ |
               |   | ref para B  | ---> +-------------------+
               |   +-------------+ |    | Objeto B          |
               +-------------------+    |   Valor: 10       |
                                        +-------------------+

Cópia Rasa

Quando A_rasa = copia_rasa(A):

Stack:         Heap:
+---+          +-------------------+
| A | -------> | Objeto A          |
+---+          |   +-------------+ |
               |   | ref para B  | ---> +-------------------+
               |   +-------------+ |    | Objeto B          |
+-----------+  +-------------------+    |   Valor: 10       |
| A_rasa    | --+-------------------+    +-------------------+
+-----------+   | Objeto A_rasa     |
                |   +-------------+ |
                |   | ref para B  | --^
                |   +-------------+ |
                +-------------------+

Observe que o Objeto B é compartilhado. Modificar o Objeto B através de A ou A_rasa afetará ambos.

Cópia Profunda

Quando A_profunda = copia_profunda(A):

Stack:         Heap:
+---+          +-------------------+
| A | -------> | Objeto A          |
+---+          |   +-------------+ |
               |   | ref para B  | ---> +-------------------+
               |   +-------------+ |    | Objeto B          |
               +-------------------+    |   Valor: 10       |
                                        +-------------------+

+-----------+  +-------------------+
| A_profunda| -->| Objeto A_profunda |
+-----------+    |   +-------------+ |
               |   | ref para B_novo| ---> +-------------------+
               |   +-------------+ |    | Objeto B_novo     |
               +-------------------+    |   Valor: 10       |
                                        +-------------------+

Aqui, Objeto B e Objeto B_novo são entidades inteiramente separadas.

Implementações e Exemplos Específicos de Linguagem

Python

O módulo copy do Python fornece copy() para cópias rasas e deepcopy() para cópias profundas. Para objetos personalizados, os métodos __copy__ e __deepcopy__ podem ser implementados.

import copy

original_list = [1, [2, 3], 4]

# Cópia Rasa
shallow_copied_list = copy.copy(original_list)
shallow_copied_list[1][0] = 99 # Modifica a lista aninhada em ambos
print(original_list)        # Saída: [1, [99, 3], 4]
print(shallow_copied_list)  # Saída: [1, [99, 3], 4]

# Cópia Profunda
deep_copied_list = copy.deepcopy(original_list)
deep_copied_list[1][0] = 100 # Modifica apenas a cópia profunda
print(original_list)        # Saída: [1, [99, 3], 4] (original permanece inalterado)
print(deep_copied_list)     # Saída: [1, [100, 3], 4]

Java

Em Java, Object.clone() realiza uma cópia rasa por padrão. Para obter uma cópia profunda, é preciso implementar manualmente a interface Cloneable e sobrescrever o método clone() para clonar recursivamente campos mutáveis, ou usar serialização.

class Course {
    String name;
    public Course(String name) { this.name = name; }
}

class Student implements Cloneable {
    String studentName;
    Course course;

    public Student(String studentName, Course course) {
        this.studentName = studentName;
        this.course = course;
    }

    // Cópia rasa via clone() padrão
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    // Implementação de cópia profunda (manual)
    public Student deepCopy() throws CloneNotSupportedException {
        Student clonedStudent = (Student) super.clone();
        clonedStudent.course = new Course(this.course.name); // Cópia profunda do objeto Course
        return clonedStudent;
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        Course math = new Course("Matemática");
        Student originalStudent = new Student("Alice", math);

        // Cópia Rasa
        Student shallowCopyStudent = (Student) originalStudent.clone();
        shallowCopyStudent.course.name = "Física"; // Também altera originalStudent.course.name
        System.out.println(originalStudent.course.name); // Saída: Física

        // Cópia Profunda
        Course history = new Course("História");
        Student originalStudent2 = new Student("Bob", history);
        Student deepCopyStudent = originalStudent2.deepCopy();
        deepCopyStudent.course.name = "Química"; // Altera apenas deepCopyStudent.course.name
        System.out.println(originalStudent2.course.name); // Saída: História
    }
}

JavaScript

JavaScript não possui uma função de cópia profunda embutida. Cópias rasas podem ser obtidas usando o operador spread (...), Object.assign(), ou Array.prototype.slice(). Cópias profundas tipicamente exigem JSON.parse(JSON.stringify()) (com limitações para funções, undefined, objetos Date, etc.) ou bibliotecas externas como _.cloneDeep() do Lodash.

const originalObject = {
  a: 1,
  b: {
    c: 2
  }
};

// Cópia Rasa
const shallowCopyObject = { ...originalObject };
shallowCopyObject.b.c = 99; // Também modifica originalObject.b.c
console.log(originalObject.b.c); // Saída: 99

// Cópia Profunda (com limitação de serialização JSON)
const deepCopyObject = JSON.parse(JSON.stringify(originalObject));
deepCopyObject.b.c = 100; // Modifica apenas deepCopyObject.b.c
console.log(originalObject.b.c); // Saída: 99 (original permanece inalterado)

Go

Em Go, a atribuição (=) cria uma cópia rasa para structs. Slices e maps são tipos de referência, então atribuí-los também cria uma cópia rasa da referência. A cópia profunda requer iteração manual ou serialização/desserialização personalizada.

package main

import (
	"fmt"
)

type Address struct {
	City string
}

type Person struct {
	Name    string
	Address *Address // Ponteiro para Address
}

func main() {
	originalAddress := &Address{"Nova Iorque"}
	originalPerson := Person{"Alice", originalAddress}

	// Cópia Rasa (atribuição de struct)
	shallowCopyPerson := originalPerson
	shallowCopyPerson.Address.City = "Londres" // Também modifica originalPerson.Address.City
	fmt.Println(originalPerson.Address.City)    // Saída: Londres

	// Cópia Profunda (manual)
	originalAddress2 := &Address{"Paris"}
	originalPerson2 := Person{"Bob", originalAddress2}

	deepCopyAddress := &Address{originalPerson2.Address.City}
	deepCopyPerson := Person{originalPerson2.Name, deepCopyAddress}

	deepCopyPerson.Address.City = "Roma" // Modifica apenas deepCopyPerson.Address.City
	fmt.Println(originalPerson2.Address.City) // Saída: Paris
}

C++

Em C++, a atribuição (=) para classes personalizadas realiza uma cópia rasa por padrão (cópia membro a membro). Para obter uma cópia profunda, um construtor de cópia e um operador de atribuição de cópia personalizados devem ser implementados para lidar com a alocação de memória dinâmica e copiar recursivamente objetos aninhados. A Regra dos Três/Cinco/Zero se aplica aqui.

#include <iostream>
#include <string>

class Course {
public:
    std::string name;
    Course(std::string n) : name(n) {}
};

class Student {
public:
    std::string studentName;
    Course* course; // Ponteiro para Course

    Student(std::string sn, Course* c) : studentName(sn), course(c) {}

    // Construtor de cópia padrão (cópia rasa)
    Student(const Student& other) : studentName(other.studentName), course(other.course) {
        std::cout << "Construtor de cópia rasa chamado\n";
    }

    // Construtor de cópia profunda
    Student deepCopy() {
        std::cout << "Cópia profunda realizada\n";
        return Student(this->studentName, new Course(*this->course)); // Cria novo objeto Course
    }

    ~Student() { // Destrutor para liberar memória para o curso
        // Apenas exclua se este objeto possuir o ponteiro do curso
        // Isso destaca a complexidade do gerenciamento manual de memória com cópias rasas/profundas
        // Para simplificar, neste exemplo, assumiremos a propriedade para deepCopy()
        // Em C++ do mundo real, ponteiros inteligentes são preferidos.
        // delete course; 
    }
};

int main() {
    Course* math = new Course("Matemática");
    Student originalStudent("Alice", math);

    // Cópia Rasa (usando o construtor de cópia padrão)
    Student shallowCopyStudent = originalStudent; // Chama o construtor de cópia
    shallowCopyStudent.course->name = "Física"; // Também modifica originalStudent.course->name
    std::cout << originalStudent.course->name << std::endl; // Saída: Física

    // Cópia Profunda
    Course* history = new Course("História");
    Student originalStudent2("Bob", history);
    Student deepCopyStudent = originalStudent2.deepCopy();
    deepCopyStudent.course->name = "Química"; // Modifica apenas deepCopyStudent.course->name
    std::cout << originalStudent2.course->name << std::endl; // Saída: História

    delete math;
    delete history;
    // Nota: deepCopyStudent.course também precisa ser excluído se não estiver usando ponteiros inteligentes
    // delete deepCopyStudent.course; // Isso seria necessário em um exemplo completo

    return 0;
}

Rust

Rust distingue entre os traits Copy e Clone. O trait Copy é para tipos que podem ser duplicados simplesmente copiando bits (por exemplo, tipos primitivos, structs contendo apenas tipos Copy). O trait Clone é para tipos que exigem uma cópia profunda e deve ser explicitamente implementado. Para tipos que implementam Copy, a atribuição realiza uma cópia bit a bit. Para tipos que implementam Clone, o método clone() realiza uma cópia profunda por convenção.

#[derive(Debug, Clone)] // Deriva Clone para cópia profunda
struct Address {
    city: String,
}

#[derive(Debug, Clone)] // Deriva Clone para cópia profunda
struct Person {
    name: String,
    address: Address,
}

fn main() {
    let original_address = Address { city: String::from("Nova Iorque") };
    let original_person = Person { name: String::from("Alice"), address: original_address.clone() };

    // Cópia Rasa (atribuição para tipos que não são Copy, como String, significa mover)
    // Para structs com campos não-Copy, a atribuição é um movimento, não uma cópia rasa.
    // Para demonstrar o comportamento de cópia rasa, precisamos clonar explicitamente a struct externa
    // mas não as partes mutáveis internas, o que não é idiomático em Rust para dados mutáveis.
    // O sistema de propriedade do Rust torna a cópia rasa direta difícil de ilustrar sem violar as regras de propriedade.
    // Em vez disso, focaremos em `Clone` para cópia profunda.

    // Cópia Profunda usando o trait Clone
    let deep_copy_person = original_person.clone();
    deep_copy_person.address.city = String::from("Londres"); // Modifica apenas deep_copy_person
    println!("Pessoa original: {:?}", original_person); // Saída: Pessoa original: Person { name: "Alice", address: Address { city: "Nova Iorque" } }
    println!("Pessoa com cópia profunda: {:?}", deep_copy_person); // Saída: Pessoa com cópia profunda: Person { name: "Alice", address: Address { city: "Londres" } }

    // Exemplo com trait Copy (para tipos primitivos)
    let x = 5;
    let y = x; // y é uma cópia de x, x ainda é válido
    println!("x: {}, y: {}", x, y);
}

Análise em Nível Binário

No nível mais baixo, a memória é uma sequência contígua de bytes. As variáveis contêm valores, que podem ser os próprios dados (para primitivos) ou um endereço de memória (para referências/ponteiros para objetos).

Quando ocorre uma cópia rasa, os bytes que representam as referências para objetos aninhados são copiados. Os bytes de dados reais dos objetos aninhados não são duplicados. Isso significa que dois objetos distintos agora contêm endereços de memória idênticos apontando para a mesma estrutura de dados subjacente.

Para uma cópia profunda, o processo é mais envolvido. Ele envolve a travessia do grafo de objetos, começando pelo objeto de nível superior. Para cada objeto aninhado encontrado, nova memória é alocada na heap, e os dados do objeto aninhado original são copiados byte a byte para este novo local de memória. Este processo recursivo garante que todos os componentes do novo objeto sejam distintos do original, residindo em seus próprios segmentos de memória alocados.

Stack vs. Heap Revisitados

  • Stack (Pilha): Armazena variáveis locais, parâmetros de função e endereços de retorno. A alocação/desalocação de memória é automática e rápida. Valores primitivos são frequentemente armazenados diretamente na stack.
  • Heap (Monte): Armazena objetos e estruturas de dados alocados dinamicamente cujo tamanho pode não ser conhecido em tempo de compilação ou cuja vida útil se estende além do escopo de uma única chamada de função. Objetos criados com new (Java, C++), malloc (C) ou implicitamente (Python, JavaScript) residem aqui. Variáveis na stack frequentemente contêm ponteiros para esses objetos alocados na heap.

Uma cópia rasa duplica a referência alocada na stack, mas o objeto alocado na heap permanece singular. Uma cópia profunda envolve novas alocações na heap para todos os objetos duplicados.

Considerações de Desempenho

  • Cópia Rasa: Geralmente mais rápida, pois envolve apenas a cópia de referências (ponteiros), que é uma operação de tamanho fixo, independentemente da complexidade dos objetos aninhados. Não requer alocação de memória adicional para dados aninhados.
  • Cópia Profunda: Pode ser significativamente mais lenta e consumir mais memória, especialmente para grafos de objetos grandes e profundamente aninhados. Envolve travessia recursiva, novas alocações de memória para cada objeto aninhado e cópia de dados byte a byte. O impacto no desempenho pode ser substancial, tornando-o uma consideração para aplicações críticas de desempenho.

Bugs do Mundo Real Causados por Cópias Rasas

Cópias rasas são uma fonte comum de bugs sutis, particularmente quando os desenvolvedores não estão cientes das referências compartilhadas. Exemplos incluem:

  1. Alterações Inesperadas de Estado: Modificar um objeto de configuração que foi copiado rasamente de um modelo padrão. Quaisquer alterações em listas ou dicionários aninhados na configuração copiada alteram inadvertidamente o modelo padrão, afetando outras partes da aplicação que dependem do padrão original.
  2. Corrupção de Dados em Ambientes Multi-threaded: Em programação concorrente, se vários threads operam em cópias rasas de um objeto, e essas cópias compartilham dados mutáveis aninhados, condições de corrida podem ocorrer, levando à corrupção de dados ou estados inconsistentes.
  3. Problemas de Re-renderização de Componentes de UI: Em frameworks de front-end, se um componente recebe uma cópia rasa de props contendo objetos mutáveis, e esses objetos aninhados são modificados diretamente, o framework pode não detectar uma alteração nas props (porque a própria referência não mudou), falhando em re-renderizar o componente com os dados atualizados.

Quando a Cópia Profunda é um Code Smell

Embora as cópias profundas forneçam independência completa, seu uso excessivo pode indicar um code smell (mau cheiro de código):

  • Sobrecarga de Desempenho: Como observado, a cópia profunda pode ser cara. Se uma cópia profunda for realizada frequentemente em grandes estruturas de dados sem uma necessidade clara de independência completa, ela pode se tornar um gargalo de desempenho.
  • Aumento do Consumo de Memória: Duplicar grafos de objetos inteiros consome significativamente mais memória. O excesso de cópia profunda pode levar a um maior uso de memória e, potencialmente, a erros de falta de memória.
  • Complexidade e Manutenção: Implementar lógica de cópia profunda personalizada (especialmente em linguagens sem suporte embutido) pode ser complexo e propenso a erros. Adiciona código boilerplate e aumenta a carga de manutenção.
  • Quebra da Identidade do Objeto: Se a identidade do objeto for crucial (por exemplo, para cache, identificadores únicos ou estruturas de grafo), a cópia profunda pode quebrar essas relações criando objetos novos e distintos.

Frequentemente, a necessidade de cópia profunda sugere que o design do objeto pode ser problemático, ou que o fluxo de dados poderia ser gerenciado de forma mais eficaz, talvez através de estruturas de dados imutáveis ou semânticas de propriedade mais claras.

Quando Usar Cada Tipo de Cópia

Característica Cópia Rasa (Shallow Copy) Cópia Profunda (Deep Copy)
Independência Novo objeto, mas objetos mutáveis aninhados são compartilhados Objeto completamente independente e todos os seus dados aninhados
Desempenho Mais rápida (copia referências) Mais lenta (recursiva, aloca nova memória)
Uso de Memória Menor (compartilha dados aninhados) Maior (duplica todos os dados aninhados)
Casos de Uso Copiar objetos simples, dados aninhados imutáveis, ou quando referências compartilhadas são desejadas Quando a isolação completa do original é necessária, especialmente com objetos mutáveis aninhados
Efeitos Colaterais Alterações em objetos mutáveis aninhados afetam tanto o original quanto a cópia Alterações afetam apenas o objeto copiado

Conclusão

A escolha entre uma cópia rasa e uma cópia profunda não é arbitrária; é uma decisão de design deliberada com implicações significativas para o comportamento do programa, desempenho e uso de memória. Uma cópia rasa é eficiente para objetos simples ou quando referências compartilhadas a dados mutáveis aninhados são aceitáveis ou até desejadas. Por outro lado, uma cópia profunda é essencial quando a independência completa do objeto original, incluindo todos os seus componentes aninhados, é primordial. Uma compreensão completa desses mecanismos de cópia, juntamente com a consciência do gerenciamento de memória e dos comportamentos específicos da linguagem, capacita os desenvolvedores a fazer escolhas informadas, prevenir bugs sutis e construir sistemas de software mais robustos e de fácil manutenção.

Criado com Hugo
Tema Stack desenvolvido por Jimmy