Copiar uma List<T> em C# se resolve com uma única expressão: List<string> copy = [.. original]; nos dá uma nova lista com os mesmos elementos. O construtor List<T> e o ToList() do LINQ fazem exatamente a mesma coisa.

Para baixar o código-fonte deste artigo, você pode acessar nosso repositório no GitHub.

O porém está no significado de “os mesmos elementos”. Se os elementos são tipos de valor que não contêm referências, ou tipos imutáveis como string, a cópia é independente e não há mais nada em que pensar. Se são objetos, as duas listas apontam para os mesmos objetos, então alterar um desses objetos o altera nas duas listas.

É esse segundo caso que justifica a leitura. Obter uma lista cujos objetos também são cópias se chama cópia profunda (deep copy), e o C# não tem um método único para isso, porque o significado de “uma cópia” depende do tipo que está na lista.

Como copiar uma lista de tipos de valor em C#?

Copiar uma lista de tipos de valor se resolve com uma única expressão. List<string> clone = [.. toppings]; aloca uma nova lista e copia todos os elementos para ela.

Outras cinco formas fazem o mesmo trabalho. O construtor List<T> aceita qualquer IEnumerable<T>, o ToList() do LINQ lê qualquer sequência, AddRange() preenche uma lista que já existe, GetRange() copia um trecho de uma lista e ConvertAll() copia enquanto muda o tipo dos elementos.

Todas elas copiam os próprios elementos, porque é assim que funciona um tipo de valor: a variável guarda o valor em si, e não uma referência a ele. Altere o clone e o original fica intacto.

string é a exceção que se comporta como a regra. É um tipo de referência, então a lista copiada compartilha os objetos string, e, como é imutável, nenhum código consegue usar a cópia para alterar o texto original.

CopyTo() é o ponto fora da curva. Ele preenche um array que já dimensionamos, então só recorra a ele quando o destino precisar ser um array.

Todos os exemplos desta seção copiam a mesma List<T> de ingredientes de pizza, e a distinção em que a seção se apoia é a que existe entre tipos de valor e tipos de referência. Esta é a lista:

var toppings = new List<string>
{
    "Mozzarella",
    "Olive oil",
    "Basil"
};

Aqui, definimos uma variável toppings do tipo List<string>, que guarda alguns dos ingredientes da clássica pizza Margherita.

A seguir, vamos ver as opções para clonar os valores dessa List<string> para outra.

Com o construtor da lista

Uma das sobrecargas do construtor de List<T> recebe um IEnumerable<T>:

var toppingsClonedWithConstructor = new List<string>(toppings);

Aqui, inicializamos uma nova variável toppingsClonedWithConstructor que contém os elementos copiados da lista toppings.

Com o método CopyTo da lista

List<T> tem vários métodos que podemos usar para clonar seu conteúdo em outra lista ou coleção.

Um deles é o método CopyTo. A sobrecarga de um argumento que chamamos aqui é definida pela própria lista, e podemos usá-la para clonar List<T> em um T[]:

var toppingsClonedWithCopyTo = new string[toppings.Count];
toppings.CopyTo(toppingsClonedWithCopyTo);

Primeiro, inicializamos uma variável toppingsClonedWithCopyTo como um string[] com tamanho igual ao da lista toppings, daí o toppings.Count. Depois, usamos o método CopyTo na lista toppings e passamos o array recém-inicializado como parâmetro. Assim, o conteúdo da lista é copiado para toppingsClonedWithCopyTo.

Se o destino é um array que já temos, a tarefa é a mesma de copiar elementos para um array, e o array precisa ser dimensionado antes da chamada.

Com o método AddRange da lista

Outro método que recebe um IEnumerable<T> como parâmetro é o AddRange:

var toppingsClonedWithAddRange = new List<string>();
toppingsClonedWithAddRange.AddRange(toppings);

O método AddRange precisa de uma List<T> já inicializada, então declaramos a variável toppingsClonedWithAddRange e atribuímos a ela uma List<string> vazia. Depois, passamos toppings como parâmetro para o método AddRange, e ele clona o conteúdo para nós.

Com o método ToList de Enumerable

O namespace System.Linq nos oferece o método Enumerable.ToList:

var toppingsClonedWithToList = toppings.ToList();

Aqui, inicializamos diretamente a variável toppingsClonedWithToList chamando o método ToList na variável toppings, que já existe, e isso clona o conteúdo dela para a nova lista.

Outra opção é usar, da mesma forma, o método ToArray da própria lista, se quisermos clonar o conteúdo de uma List<T> para T[].

Com o método ConvertAll

List<T> tem outro método útil que, à primeira vista, pode parecer um pouco intimidador: o método ConvertAll<TOutput>(Converter<T, TOutput>). Ele converte todos os elementos de uma lista de um tipo para outro e retorna uma lista com os elementos convertidos. Podemos até usá-lo para clonar:

var toppingsClonedWithConvertAll = toppings
    .ConvertAll(new Converter<string, string>(x => x));

Começamos inicializando uma variável toppingsClonedWithConvertAll e atribuindo a ela o valor retornado pelo método ConvertAll chamado na lista toppings. O método recebe um Converter<TInput, TOutput>, que é apenas um delegado para um método que converte um elemento de um tipo para outro. Ele recebe o nome de um método usado na conversão, mas também podemos passar um método anônimo.

Não precisamos de um método separado para converter o elemento que passamos, então simplesmente usamos uma expressão lambda que retorna o mesmo valor, daí o x => x.

Com uma expressão de coleção

Desde o C# 12, as expressões de coleção nos dão a forma mais curta de todas:

List<string> toppingsClonedWithCollectionExpression = [.. toppings];

O elemento de propagação (spread), .., copia todos os elementos de toppings para uma lista totalmente nova.

O que merece atenção é o tipo de destino escrito por extenso. Uma expressão de coleção não tem tipo próprio, então precisa de um tipo para o qual possa ser convertida: escreva var aqui e o compilador responde com error CS9176: There is no target type for the collection expression..

The Web API Production Checklist, e-book gratuito em inglês

E-book gratuito

Sua Web API está pronta para produção?

33 itens para verificar antes de implantá-la, com a correção de cada um. Um PDF gratuito de 76 páginas para .NET 10.

O e-book está em inglês.

Baixe o checklist gratuito

PDF gratuito. Um único e-mail para enviá-lo. Cancele a inscrição quando quiser.

Também podemos copiar parte de uma lista, em vez da lista inteira, com o método GetRange:

var toppingsClonedWithGetRange = toppings.GetRange(0, toppings.Count);

Aqui, passamos o índice inicial e o número de elementos que queremos, então passar 0 e toppings.Count copia a lista inteira para uma nova List<string>.

Para garantir que tudo o que fizemos para copiar uma lista de strings em C# funciona como esperado, podemos imprimir todos os resultados no console:

Console.WriteLine("Original list: " + string.Join(", ", toppings));
Console.WriteLine("Cloned with Constructor: " + string.Join(", ", toppingsClonedWithConstructor));
Console.WriteLine("Cloned with CopyTo: " + string.Join(", ", toppingsClonedWithCopyTo));
Console.WriteLine("Cloned with AddRange: " + string.Join(", ", toppingsClonedWithAddRange));
Console.WriteLine("Cloned with ToList: " + string.Join(", ", toppingsClonedWithToList));
Console.WriteLine("Cloned with ConvertAll: " + string.Join(", ", toppingsClonedWithConvertAll));
Console.WriteLine("Cloned with a collection expression: " + string.Join(", ", toppingsClonedWithCollectionExpression));
Console.WriteLine("Cloned with GetRange: " + string.Join(", ", toppingsClonedWithGetRange));

E conferir o resultado:

Original list: Mozzarella, Olive oil, Basil
Cloned with Constructor: Mozzarella, Olive oil, Basil
Cloned with CopyTo: Mozzarella, Olive oil, Basil
Cloned with AddRange: Mozzarella, Olive oil, Basil
Cloned with ToList: Mozzarella, Olive oil, Basil
Cloned with ConvertAll: Mozzarella, Olive oil, Basil
Cloned with a collection expression: Mozzarella, Olive oil, Basil
Cloned with GetRange: Mozzarella, Olive oil, Basil

Como fazer uma cópia profunda de uma lista de tipos de referência em C#?

Com tipos de referência, as seis formas acima continuam funcionando, mas já não nos dão uma lista independente. Cada uma produz uma nova lista com as mesmas referências de objeto: duas listas, um único conjunto de objetos. Limpe os ingredientes de uma pizza por qualquer uma das listas e as duas listas mostram a pizza sem ingredientes. Isso é uma cópia superficial (shallow copy).

A alternativa é a cópia profunda, que é construída um elemento por vez:

List<Pizza> clone = [.. pizzas.Select(p => new Pizza(p))];

O trabalho fica no tipo do elemento, não na lista. Um construtor de cópia em Pizza decide o que significa uma cópia de pizza, e também precisa copiar a lista Toppings, porque essa propriedade é outra referência.

Por isso, a cópia profunda é recursiva por natureza. Cada objeto mutável que um elemento guarda precisa do mesmo tratamento, em todos os níveis, e esse é o motivo pelo qual nenhum método do framework, sozinho, consegue fazer isso por nós.

A cópia superficial é mais barata e costuma bastar. Escolha a profunda quando os objetos precisarem divergir.

Continuando com o tema das pizzas de antes, vamos ampliar o exemplo:

public class Pizza
{
    public required string Name { get; set; }
    public required List<string> Toppings { get; set; }

    public override string ToString()
    {
        return $"Pizza name: {Name}; Toppings: {string.Join(", ", Toppings)}";
    }
}

Em um arquivo separado, declaramos a classe Pizza com duas propriedades simples, Name e Toppings, ambas required, para que nenhuma pizza possa ser criada sem elas. Também sobrescrevemos (override) o método ToString para facilitar a visualização.

Estamos clonando listas, então precisamos de uma com tipos de referência:

var pizzas = new List<Pizza>
{
    new Pizza
    {
        Name= "Margherita",
        Toppings = new List<string>
        {
            "Mozzarella",
            "Olive oil",
            "Basil"
        }
    },
    new Pizza
    {
        Name= "Diavola",
        Toppings = new List<string>
        {
            "Mozzarella",
            "Ventricina",
            "Chili peppers"
        }
    }
};

Aqui, declaramos uma variável pizzas do tipo List<Pizza> e adicionamos duas pizzas a ela.

Agora, vejamos os dois tipos de cópia que existem quando lidamos com tipos de referência.

Cópia superficial

Podemos obter facilmente uma cópia superficial da lista pizzas com qualquer um dos métodos da seção anterior. Mas uma cópia superficial de uma List<T>, em que T é um tipo de referência, copia apenas a estrutura da coleção e as referências aos elementos, não os próprios elementos. Isso significa que alterações nos elementos de qualquer uma das duas listas vão aparecer tanto na lista original quanto na cópia.

Vamos ilustrar isso:

var clonedPizzas = pizzas.ToList();

var margherita = pizzas
    .First(x => x.Name == "Margherita");

margherita.Toppings.Clear();

Primeiro, criamos uma variável clonedPizzas e clonamos nela o conteúdo de pizzas com o método ToList (qualquer um dos outros métodos que usamos antes produz o mesmo resultado). Depois, obtemos a pizza Margherita da lista original com o método First. Por fim, usamos o método Clear para esvaziar a lista Toppings dessa pizza.

Agora, vejamos o que acontece:

Console.WriteLine($"Original Margherita: {pizzas.First()}");
Console.WriteLine($"Cloned with ToList: {clonedPizzas.First()}");

Com o método First, imprimimos no console a primeira pizza de cada lista, que nos dois casos é a Margherita.

Sobrescrevemos o método ToString, que nos dá uma representação fácil de ler de cada pizza. Agora, podemos conferir o resultado:

Original Margherita: Pizza name: Margherita; Toppings:
Cloned with ToList: Pizza name: Margherita; Toppings:

Vemos que tanto a pizza Margherita original quanto a copiada agora têm a lista Toppings vazia. Isso acontece porque, ao criar uma cópia superficial, clonamos apenas as referências aos objetos, não os objetos em si. Não é o ideal, porque, quando alteramos elementos em uma lista, alteramos esses elementos em todos os lugares para onde copiamos a lista.

Isso pode nos causar problemas sérios, então vejamos o que podemos fazer para evitá-lo.

Antes de continuar, devolvemos os ingredientes à Margherita, para que as cópias profundas que faremos a seguir partam da lista completa:

margherita.Toppings.AddRange(["Mozzarella", "Olive oil", "Basil"]);

O que muda com uma cópia profunda

A alternativa é criar uma cópia profunda, o que significa que não copiamos apenas as referências aos objetos, mas criamos objetos novos, copiados. Isso produz um resultado diferente do das cópias superficiais, porque os objetos referenciados pela lista copiada são distintos dos referenciados pela lista original.

Não existe um método do framework que faça isso por nós, então as duas técnicas abaixo constroem a nova lista um elemento por vez, seja com um loop foreach, seja com a projeção de uma linha [.. pizzas.Select(p => new Pizza(p))]. A única diferença entre elas é a forma como cada elemento é copiado.

Vale comentar os records aqui, porque eles parecem ser a resposta, mas não são: uma expressão with copia as referências, não os objetos, então um record que contém uma List<string> continua compartilhando essa lista com a instância da qual foi copiado.

The Web API Production Checklist, e-book gratuito em inglês

E-book gratuito

Sua Web API está pronta para produção?

33 itens para verificar antes de implantá-la, com a correção de cada um. Um PDF gratuito de 76 páginas para .NET 10.

O e-book está em inglês.

Baixe o checklist gratuito

PDF gratuito. Um único e-mail para enviá-lo. Cancele a inscrição quando quiser.

Cópia profunda com um construtor de cópia

Um construtor de cópia é um construtor que recebe uma instância do próprio tipo como parâmetro. Em seguida, os valores de cada propriedade desse objeto são copiados para a instância recém-criada desse tipo:

public Pizza() { }

[SetsRequiredMembers]
public Pizza(Pizza pizza)
{
    Name = pizza.Name;
    Toppings = pizza.Toppings.ToList();
}

Na classe Pizza, criamos um novo construtor que recebe um objeto Pizza como parâmetro. Nele, atribuímos às propriedades Name e Toppings os valores do objeto recebido, lembrando que precisamos passar uma cópia de Toppings, feita com pizza.Toppings.ToList(), e não apenas atribuir o valor.

O atributo [SetsRequiredMembers], do namespace System.Diagnostics.CodeAnalysis, está ali porque as duas propriedades são required. Ele informa ao compilador que este construtor define as duas, então quem chama o construtor não precisa repeti-las em um inicializador de objeto. Declarar um construtor também faz o C# deixar de fornecer um construtor sem parâmetros, do qual os nossos inicializadores de objeto ainda precisam; por isso, adicionamos também um construtor Pizza() vazio.

Agora, podemos voltar à classe Program e fazer a clonagem:

List<Pizza> pizzasClonedWithCopyConstructor = [.. pizzas.Select(p => new Pizza(p))];

Projetamos cada pizza da lista pelo construtor de cópia e reunimos os resultados em uma nova lista com uma expressão de coleção. Essa é a cópia profunda completa: uma nova lista e um novo objeto Pizza para cada elemento dela.

Cópia profunda com a interface ICloneable

Também podemos usar o método Clone, que escrevemos ao implementar a interface ICloneable, para criar uma cópia profunda. Ele nos dá um nome convencional para a operação que o construtor de cópia já faz, o que vale a pena em código que já usa a interface, e é algo para conhecer, não para usar como ponto de partida: como mostra a seção no fim deste artigo, a Microsoft recomenda não implementá-la em APIs públicas.

Vamos fazer as alterações necessárias:

using System.Diagnostics.CodeAnalysis;

public class Pizza : ICloneable
{
    public required string Name { get; set; }
    public required List<string> Toppings { get; set; }

    public Pizza() { }

    [SetsRequiredMembers]
    public Pizza(Pizza pizza)
    {
        Name = pizza.Name;
        Toppings = pizza.Toppings.ToList();
    }

    public object Clone()
    {
        return new Pizza
        {
            Name = Name,
            Toppings = Toppings.ToList(),
        };
    }

    public override string ToString()
    {
        return $"Pizza name: {Name}; Toppings: {string.Join(", ", Toppings)}";
    }
}

Ampliamos a classe Pizza implementando a interface ICloneable e o método Clone dela. Desta vez, criamos uma implementação que retorna um novo objeto Pizza e copia as propriedades da instância atual: atribuímos Name diretamente, porque é uma string, e depois usamos o método ToList, que vimos antes, para obter um clone da lista Toppings.

Depois, partimos para a clonagem:

var pizzasClonedWithICloneable = new List<Pizza>();

foreach (var pizza in pizzas)
{
    pizzasClonedWithICloneable.Add((Pizza)pizza.Clone());
}

Para isso, criamos uma variável pizzasClonedWithICloneable como uma List<Pizza> vazia. Depois, em um loop foreach que percorre a lista pizzas inicial, obtemos uma nova cópia da pizza atual com o método Clone e a adicionamos à nova lista.

Agora que usamos os dois métodos de criar uma cópia profunda, vamos testá-los:

margherita = pizzas
    .First(x => x.Name == "Margherita");

margherita.Toppings.Clear();

Console.WriteLine($"Original Margherita: {pizzas.First()}");
Console.WriteLine($"Cloned with ICloneable: {pizzasClonedWithICloneable.First()}");
Console.WriteLine($"Cloned with Copy Constructor: {pizzasClonedWithCopyConstructor.First()}");

Depois do loop foreach na classe Program, obtemos novamente a pizza Margherita e limpamos a lista Toppings dela. Em seguida, imprimimos as pizzas Margherita no console e examinamos o resultado:

Original Margherita: Pizza name: Margherita; Toppings:
Cloned with ICloneable: Pizza name: Margherita; Toppings: Mozzarella, Olive oil, Basil
Cloned with Copy Constructor: Pizza name: Margherita; Toppings: Mozzarella, Olive oil, Basil

Desta vez, vemos que conseguimos criar uma cópia profunda ao clonar a lista, e as pizzas clonadas mantêm a lista Toppings intacta, já que de fato criamos uma cópia profunda com as duas abordagens.

Qual forma de copiar uma lista usar?

Comece por uma pergunta. As duas listas precisam conter objetos diferentes ou apenas ser listas diferentes?

Se apenas as listas precisam ser diferentes, faça uma cópia superficial e escreva-a como uma expressão de coleção: List<string> clone = [.. toppings];. O construtor List<T> e ToList() dão exatamente o mesmo resultado, então prefira-os quando a origem for uma consulta LINQ ou quando o projeto usar uma versão mais antiga da linguagem.

Se os objetos também precisam ser diferentes, a cópia é responsabilidade do tipo do elemento. Dê a esse tipo um construtor de cópia e projete a lista por meio dele.

ICloneable é a opção a evitar em código novo. A própria orientação da Microsoft é direta: como quem chama Clone() não pode contar com uma operação de clonagem previsível, ela recomenda que ICloneable não seja implementada em APIs públicas. A interface não diz se um Clone() é profundo ou superficial, então quem o chama não tem como saber.

A orientação vem citada, e não parafraseada, da própria documentação da interface: “Como quem chama Clone() não pode contar com o método para executar uma operação de clonagem previsível, recomendamos que ICloneable não seja implementada em APIs públicas.”

Tudo isso trata de listas. Para fazer uma cópia profunda de um único objeto, incluindo o método MemberwiseClone e as abordagens com serialização e reflexão, esse é o artigo para ler em seguida.

A diferença é mais fácil de ver do que de descrever: conte as setas que chegam a cada pizza.

Dois painéis. No painel da cópia superficial, uma caixa com o rótulo pizzas e uma caixa com o rótulo clone têm setas que chegam aos mesmos dois objetos pizza, Margherita e Diavola, então cada objeto recebe duas setas. No painel da cópia profunda, pizzas aponta para a Margherita e a Diavola originais, e clone aponta para dois objetos novos e separados com os mesmos nomes, desenhados em outra cor.

Estas são as nove formas que este artigo mostrou, com as duas perguntas que decidem a escolha: a abordagem nos entrega uma nova lista? E novos elementos?

AbordagemCódigoNova lista?Novos elementos?Use quando
Expressão de coleçãoList<string> copy = [.. toppings];SimNãoÉ o padrão para uma cópia superficial
Construtor List<T>new List<string>(toppings)SimNãoO mesmo resultado em qualquer versão da linguagem
ToList()toppings.ToList()SimNãoA origem é uma consulta LINQ ou um IEnumerable<T>
AddRange()copy.AddRange(toppings)Não, preenche uma lista que criamosNãoAdicionar a uma lista que já existe
GetRange() ou toppings[..]toppings.GetRange(0, toppings.Count)SimNãoCopiar parte da lista, não a lista inteira
CopyTo()toppings.CopyTo(array)Não, preenche um array que dimensionamosNãoO destino precisa ser um array
ConvertAll()toppings.ConvertAll(t => t)SimSó se o conversor os criarMudar o tipo dos elementos durante a cópia
Construtor de cópia, com projeção[.. pizzas.Select(p => new Pizza(p))]SimSimUma cópia profunda, e o tipo do elemento é nosso
ICloneable.Clone(), com projeção[.. pizzas.Select(p => (Pizza)p.Clone())]SimSó se Clone() for escrito para issoO código existente já a implementa

Conclusão

Neste artigo, aprendemos tudo sobre como copiar e clonar uma List em C#. Também vimos que cópias superficiais são fáceis de obter de várias formas e funcionam às mil maravilhas com tipos de valor, mas podem dar dor de cabeça com tipos de referência. Cópias profundas são muito úteis, mas tendem a ser mais caras que as superficiais, porque exigem a criação de objetos adicionais. Elas também podem ser muito complicadas de obter quando lidamos com objetos muito complexos.

Se for para guardar uma única frase, que seja esta: uma expressão de coleção copia a lista, e um construtor de cópia no tipo do elemento copia o que está dentro dela.

Testado com .NET 10.0.10.