Clonando objetos no C#
Clonar objetos em C# parece trivial à primeira vista, mas envolve nuances de semântica de valor, de referência e de imutabilidade. Neste artigo, vamos explorar os aspectos fundamentais do tema, dos conceitos básicos até as práticas mais avançadas de clonagem.
Ao final, você vai saber quando uma atribuição simples basta, quando usar uma cópia superficial e quando a cópia profunda é obrigatória. Além disso, vai entender como records e a expressão with simplificam esse trabalho.
Por que clonar objetos em C# exige atenção
Muitos bugs nascem de uma suposição errada: a de que var b = a; sempre cria uma cópia independente. Na prática, o resultado depende do tipo de a.
Se o tipo armazena o próprio valor, a atribuição cria uma cópia. Por outro lado, se o tipo guarda apenas um endereço, as duas variáveis apontam para o mesmo objeto.
Por isso, antes de clonar objetos em C#, precisamos entender a diferença entre tipos de valor e tipos de referência. Em seguida, veremos como cada construção da linguagem se comporta.
Tipos de valor
Tipos de valor, ou value types, armazenam diretamente os seus dados. Assim, quando você copia um tipo de valor, uma nova instância surge com os mesmos dados.
int a = 10;
int b = a; // b recebe uma cópia de a
b = 20;
Console.WriteLine(a); // 10
Alterar b não afeta a, pois as duas variáveis são independentes na memória. Esse comportamento vale para int, double, bool, char, decimal, enums e para qualquer struct.
Tipos de referência
Tipos de referência, ou reference types, armazenam uma referência para a localização do objeto na memória. Então, quando você copia um tipo de referência, copia apenas esse ponteiro.
var p1 = new Person { Name = "John" };
var p2 = p1;
p2.Name = "Mary"; // p1.Name também será "Mary"
Nesse exemplo, existe um único objeto Person e duas variáveis que apontam para ele. Ou seja, ninguém clonou nada, e qualquer alteração aparece nos dois lados.
Esse é o ponto de partida para clonar objetos em C# de forma consciente. Afinal, quando queremos independência entre as instâncias, precisamos criar um novo objeto de forma explícita.
Imutabilidade
Objetos imutáveis não podem mudar depois de criados. Isso garante segurança em ambientes concorrentes e facilita a depuração.
O tipo string é imutável, assim como o System.DateTime. Portanto, os métodos que parecem alterar uma string, na verdade, devolvem um novo objeto.
string name = "John";
name = name.Replace("J", "M"); // Novo objeto criado, o original não muda
Se quiser se aprofundar nesse caso específico, o artigo sobre strings imutáveis em C# mostra o que acontece por baixo dos panos.
A imutabilidade é um conceito essencial no design de software moderno, pois promove segurança, previsibilidade e facilidade de manutenção. Em objetos imutáveis, o estado não muda após a criação, o que evita efeitos colaterais e comportamentos inesperados.
Esses benefícios pesam ainda mais em aplicações concorrentes ou com múltiplas threads. Afinal, se ninguém altera o objeto, ninguém precisa de trava para lê-lo.
Esse modelo funciona muito bem em modelagem de domínio, programação funcional, cache de dados e passagem de objetos entre componentes. Com isso, cada parte da aplicação lida com dados consistentes e confiáveis.
Além disso, a imutabilidade reduz a necessidade de clonar objetos em C#. Se nenhum objeto muda, compartilhar a mesma referência é seguro.
Classes
Classes são tipos de referência. Portanto, ao atribuir uma instância de classe a outra variável, ambas apontam para o mesmo objeto na memória.
public class Person
{
public string Name { get; set; }
}
Isso não é um defeito, e sim uma escolha de design. Na maioria das vezes, queremos justamente compartilhar a mesma entidade entre várias partes do sistema.
O problema aparece quando uma parte do código precisa de uma versão modificada sem afetar as outras. Nesse caso, precisamos de uma das técnicas de clonagem que veremos adiante.
Para revisar os fundamentos de classes, objetos e encapsulamento, vale ler o artigo sobre orientação a objetos.
Records e a clonagem de objetos em C#
Os record são tipos que priorizam imutabilidade e igualdade estrutural. Por padrão, records são tipos de referência, assim como classes, e compartilham comportamentos semelhantes, como a atribuição por referência.
Entretanto, o C# também permite declarar records como tipos de valor com a palavra-chave struct, como em public record struct Person. Esse recurso chegou no C# 10.
Nesse caso, o record se comporta como uma struct tradicional. Essa flexibilidade permite escolher entre referência e valor com base no desempenho ou na semântica de cópia desejada.
Além disso, o compilador gera automaticamente membros como Equals, GetHashCode e ToString. Também gera o suporte à expressão with, que cria cópias com alterações pontuais nas propriedades.
public record Person(string Name);
var p1 = new Person("John");
var p2 = p1 with { Name = "Mary" }; // Clona e altera somente o nome
Segundo a documentação oficial de records, o compilador sintetiza um método de clone e um construtor de cópia. Assim, a expressão with chama o clone e depois aplica as propriedades informadas.
Por isso, os records se tornaram a forma mais idiomática de clonar objetos em C# quando o tipo representa dados. Eles eliminam boa parte do código manual de cópia.
Structs
Structs são tipos de valor. Então, quando você as atribui a uma nova variável, cria uma cópia por valor.
public struct Point
{
public int X;
public int Y;
}
var a = new Point { X = 1, Y = 2 };
var b = a; // Cópia de a
b.X = 10;
Console.WriteLine(a.X); // 1
Atenção, porém, a um detalhe importante ao clonar objetos em C# com structs. Se a struct tiver um campo de referência, a cópia leva só a referência, e não o objeto.
Por esse motivo, structs funcionam melhor para valores pequenos e autocontidos. Coordenadas, valores monetários e intervalos de datas são bons exemplos.
Como clonar objetos em C# de tipos de referência
Para clonar objetos em C# de tipos de referência, existem dois níveis de cópia. A cópia superficial duplica só o primeiro nível, enquanto a profunda duplica tudo o que ele referencia.
A documentação da Microsoft ilustra bem essa diferença. Considere um objeto X que referencia os objetos A e B, e imagine que B referencia C.
A cópia superficial de X cria um X2 que continua apontando para A e B. Já a cópia profunda cria um X2 que aponta para A2 e B2, e B2 aponta para um novo C2.
MemberwiseClone()
A forma mais básica de clonar objetos em C# é o MemberwiseClone(), um método protegido da classe Object que faz uma cópia superficial, ou shallow copy.
public class Person
{
public string Name { get; set; }
public Person Clone() => (Person)MemberwiseClone();
}
Segundo a documentação do MemberwiseClone, o método cria um novo objeto e copia os campos não estáticos do atual. Campos de tipo de valor recebem uma cópia bit a bit.
Cuidado, porém: se o objeto contiver propriedades que também são referências, o método não as duplica. Nesse caso, o clone e o original passam a compartilhar o mesmo objeto interno.
public class Address
{
public string Street { get; set; }
}
public class Customer
{
public string Name { get; set; }
public Address Address { get; set; }
public Customer ShallowCopy() => (Customer)MemberwiseClone();
}
var original = new Customer
{
Name = "John",
Address = new Address { Street = "Main Street" }
};
var copy = original.ShallowCopy();
copy.Address.Street = "Second Street";
Console.WriteLine(original.Address.Street); // Second Street
Note que o nome continua independente, mas o endereço não. Esse é o comportamento esperado de uma shallow copy.
Deep copy ao clonar objetos em C#
Quando o grafo tem referências, clonar objetos em C# exige uma cópia profunda, ou deep copy, que clona de forma recursiva todos os objetos referenciados. Assim, o clone fica totalmente independente do original.
public class Address
{
public string Street { get; set; }
}
public class Person
{
public string Name { get; set; }
public Address Address { get; set; }
public Person DeepCopy()
{
return new Person
{
Name = Name,
Address = new Address { Street = Address.Street }
};
}
}
A documentação oficial lista quatro caminhos para implementar uma deep copy. O primeiro é chamar o construtor da classe com os valores do objeto original.
O segundo é combinar o MemberwiseClone() com a criação de novos objetos para cada campo de referência. Em seguida, vêm a serialização e a desserialização do objeto e, por fim, o uso de reflection com recursão.
public Person DeepCopy()
{
// Copia superficial primeiro, depois duplica as referências
var other = (Person)MemberwiseClone();
other.Address = new Address { Street = Address.Street };
return other;
}
A abordagem manual é a mais explícita e a mais rápida. No entanto, exige manutenção sempre que a classe ganha uma nova propriedade de referência.
Serialização para clonar objetos em C#
Outra forma de clonar objetos em C# com cópia profunda é serializar o objeto e desserializar o resultado em uma nova instância. Com o System.Text.Json, isso cabe em poucas linhas.
using System.Text.Json;
public static class CloneExtensions
{
// Gera uma cópia profunda via serialização JSON
public static T DeepClone<T>(this T source)
{
var json = JsonSerializer.Serialize(source);
return JsonSerializer.Deserialize<T>(json)!;
}
}
var copy = original.DeepClone();
A vantagem é a simplicidade, porque o método funciona para qualquer tipo serializável. No entanto, essa abordagem tem custos que você precisa conhecer.
Primeiro, o desempenho é bem inferior ao de uma cópia manual. Além disso, por padrão o serializador considera apenas propriedades públicas, então campos privados ficam de fora.
Grafos com referências circulares também exigem configuração extra, como o ReferenceHandler.Preserve. Por isso, reserve essa técnica para cenários em que a praticidade compensa o custo.
ICloneable
O .NET também oferece a interface ICloneable, com um único método Clone(). Contudo, a interface não define se a cópia é superficial ou profunda.
Por isso, a própria Microsoft recomenda não implementar ICloneable em APIs públicas. Afinal, quem chama o método não consegue prever o resultado da clonagem.
Na prática, nomes explícitos como ShallowCopy() e DeepCopy() comunicam melhor a intenção. Assim, quem lê o código sabe exatamente o que esperar.
Clonar objetos em C# dentro de coleções e arrays
Coleções merecem atenção especial na hora de clonar objetos em C#. Afinal, a lista em si é um objeto de referência, e seus itens também podem ser.
O método Array.Clone(), por exemplo, gera uma cópia superficial do array. Segundo a documentação, os elementos de referência do novo array apontam para os mesmos objetos do original.
var people = new[] { new Person { Name = "John" } };
var shallow = (Person[])people.Clone();
shallow[0].Name = "Mary";
Console.WriteLine(people[0].Name); // Mary
O mesmo vale para new List<T>(original) e para o ToList(). Eles criam uma nova lista, mas os itens continuam compartilhados.
// Cópia superficial: nova lista, mesmos itens
var shallowList = new List<Person>(people);
// Cópia profunda: nova lista e novos itens
var deepList = people.Select(person => person.DeepCopy()).ToList();
Portanto, para uma cópia profunda de uma coleção, clone cada item individualmente. Dessa forma, alterações no clone não vazam para a coleção original.
With: mutação não destrutiva
Com records, clonar objetos em C# fica ainda mais simples, porque eles criam novas instâncias alteradas sem modificar o original. Esse recurso recebe o nome de mutação não destrutiva.
public record Product(string Name, decimal Price);
var p1 = new Product("Notebook", 2500);
var p2 = p1 with { Price = 2300 }; // p1 permanece intacto
Apesar de o with ter surgido com o record no C# 9, a partir do C# 10 ele também funciona com structs. Além disso, a mesma versão estendeu o suporte aos tipos anônimos.
public struct Point
{
public int X { get; set; }
public int Y { get; set; }
}
var a = new Point { X = 1, Y = 2 };
var b = a with { Y = 5 }; // a continua com Y = 2
Uma expressão with vazia também é útil. Ela cria uma cópia idêntica do record, como em var copy = original with { };.
Cuidados ao clonar objetos em C# com with
O resultado de uma expressão with é uma cópia superficial. Ou seja, se o record tiver uma propriedade de tipo de referência, o original e a cópia compartilham a mesma instância.
public record Order(string Code, List<string> Items);
var original = new Order("A1", new List<string> { "Mouse" });
var copy = original with { Code = "A2" };
copy.Items.Add("Keyboard");
Console.WriteLine(original.Items.Count); // 2
Para mudar esse comportamento em um record class, você pode declarar o próprio construtor de cópia. Dessa forma, o compilador deixa de gerar o padrão e usa o seu.
public record Order(string Code, List<string> Items)
{
// Construtor de cópia personalizado: duplica a lista
protected Order(Order original)
{
Code = original.Code;
Items = new List<string>(original.Items);
}
}
Já para record struct e structs comuns, não é possível personalizar a semântica de cópia. Nesse caso, a melhor saída é preferir coleções imutáveis nas propriedades.
Outro cuidado envolve propriedades calculadas. A documentação recomenda calculá-las no acesso, e não na criação, porque o with copia o valor antigo antes de aplicar as alterações.
Como testar se o clone é independente
Bugs de clonagem costumam aparecer longe do ponto em que a cópia nasceu. Por isso, vale proteger os métodos de cópia com testes de unidade simples.
A ideia é sempre a mesma: criar o original, gerar o clone, alterar o clone e verificar o original. Se o original mudar, a cópia não é tão profunda quanto deveria.
[Fact]
public void DeepCopy_ShouldNotShareAddress()
{
var original = new Person
{
Name = "John",
Address = new Address { Street = "Main Street" }
};
var copy = original.DeepCopy();
copy.Address.Street = "Second Street";
// O original precisa continuar intacto
Assert.Equal("Main Street", original.Address.Street);
}
Também é útil verificar a referência diretamente com Assert.NotSame. Assim, o teste falha logo se alguém trocar a deep copy por um MemberwiseClone() durante uma refatoração.
Se você ainda não escreve testes desse tipo, o artigo sobre fundamentos dos testes de unidade é um bom ponto de partida.
Qual técnica escolher para clonar objetos em C#
Não existe uma técnica única para todos os cenários. Por isso, a tabela a seguir resume quando usar cada abordagem.
| Cenário | Técnica recomendada |
|---|---|
| Valores pequenos e autocontidos | struct ou record struct |
| Dados imutáveis com pequenas variações | record com with |
| Objeto sem referências internas | MemberwiseClone |
| Objeto com grafo de referências | Deep copy manual |
| Grafos grandes e complexos | Serialização e desserialização |
Em geral, comece pela imutabilidade. Se os tipos não mudam, você precisa clonar objetos em C# com muito menos frequência.
Quando a cópia for inevitável, prefira métodos com nomes explícitos. Além disso, escreva testes que alterem o clone e verifiquem se o original continua intacto.
Esse cuidado também vale para Value Objects do domínio. O artigo sobre como criar bons Value Objects em C# mostra como a imutabilidade simplifica esse tipo de modelagem.
Conclusão
Dominar como clonar objetos em C# é essencial para evitar efeitos colaterais inesperados. Isso vale principalmente para aplicações com concorrência, paralelismo ou lógica de negócio complexa.
Compreender quando usar shallow copy, deep copy ou optar pela imutabilidade melhora bastante a qualidade do seu código. Com records e a expressão with, o C# moderno torna esse trabalho muito mais simples e seguro.
FAQ
Atribuir um objeto a outra variável cria um clone?
Não para tipos de referência. A atribuição copia apenas a referência, e as duas variáveis apontam para o mesmo objeto.
O MemberwiseClone faz cópia profunda?
Não. Ele faz uma cópia superficial, e os campos de referência continuam apontando para os mesmos objetos.
A expressão with faz cópia profunda?
Não. O resultado é uma cópia superficial, então propriedades de referência ficam compartilhadas entre o original e a cópia.
Posso usar with em classes comuns?
Não. O with funciona com records, structs e tipos anônimos, mas não com classes comuns.
Devo implementar ICloneable?
A Microsoft recomenda não implementar ICloneable em APIs públicas, porque a interface não define se a cópia é superficial ou profunda.