Updated on

Copying a List<T> in C# is one expression: List<string> copy = [.. original]; gives us a new list holding the same elements. The List<T> constructor and LINQ’s ToList() do exactly the same thing.

To download the source code for this article, you can visit our GitHub repository.

The catch is what “the same elements” means. If the elements are value types that hold no references, or immutable ones like string, the copy is independent and there is nothing more to think about. If they are objects, both lists point at the same objects, so changing one of those objects changes it in both lists.

That second case is the one worth the reading time. Getting a list whose objects are also copies is called a deep copy, and C# has no single method for it, because what “a copy” means depends on the type in the list.

How Do We Copy a List of Value Types in C#?

Copying a list of value types is one expression. List<string> clone = [.. toppings]; allocates a new list and copies every element into it.

Five other forms do the same work. The List<T> constructor takes any IEnumerable<T>, LINQ’s ToList() reads any sequence, AddRange() fills a list that already exists, GetRange() copies a slice of one, and ConvertAll() copies while changing the element type.

All of them copy the elements themselves, because that is what a value type is: the variable holds the value rather than a reference to it. Change the clone and the original does not move.

string is the exception that behaves like the rule. It is a reference type, so a copied list shares the string objects, and it is immutable, so no code can reach through the copy and alter the original text.

CopyTo() is the odd one out. It fills an array we have already sized, so reach for it only when the target has to be an array.

Every example in this section copies the same List<T> of pizza toppings, and the distinction it rests on is the one between value and reference types. Here is the list:

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

Here, we define a toppings variable of List<string> type, that holds some of the toppings for the classic Margherita pizza.

Next, we’ll look at the options for cloning the values of that List<string> to another.

Using the List’s Constructor

One of the overloads of the List<T>‘s constructor takes in an IEnumerable<T>:

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

Here, we initialize a new toppingsClonedWithConstructor variable that contains the copied elements from our toppings list.

Using the List’s CopyTo Method

List<T> has several methods that we can use to clone its contents to another list or collection.

One of those is the CopyTo method. The one-argument overload we call here is the list’s own, and it can be used to clone the List<T> to a T[]:

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

First, we initialize a toppingsClonedWithCopyTo variable as a string[] that has a length equal to the length of the toppings list – hence the toppings.Count. Then we use the CopyTo method on the toppings list and pass the newly initialized array as a parameter. This way its contents are copied over to toppingsClonedWithCopyTo.

If the target is an array we already hold, this is the same job as copying elements into an array, and the array has to be sized before the call.

Using the List’s AddRange Method

Another method that takes in an IEnumerable<T> as a parameter is the AddRange method:

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

The AddRange method needs a List<T> that is already initialized, so we declare the toppingsClonedWithAddRange variable and assign an empty List<string> to it. Then we pass the toppings as a parameter to the AddRange method and it clones the contents for us.

Using the Enumerable’s ToList Method

The System.Linq namespace provides us with the Enumerable.ToList method:

var toppingsClonedWithToList = toppings.ToList();

Here, we directly initialize our toppingsClonedWithToList variable by calling the ToList method on our already existing toppings variable, which clones its contents to our new list.

Alternatively, we can use the list’s own ToArray method in the same way if we want to clone the contents of a List<T> to T[].

Using the ConvertAll Method

List<T> has another useful method that might look a bit daunting on the surface – this is the ConvertAll<TOutput>(Converter<T, TOutput>) method. It converts all the elements of a list from one type to another and returns a list containing the converted elements. We can even use it for cloning:

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

We start by initializing a toppingsClonedWithConvertAll variable and assigning it the value returned from using the ConvertAll method on our toppings list. The method takes in a Converter<TInput, TOutput>, which is just a delegate for a method that converts an element from one type to another. It takes in the name of a method used for the conversion, or we can pass an anonymous method as well.

We don’t need a separate method to convert the element we pass in, so we simply use a lambda expression that returns the same value, hence the x => x.

Using a Collection Expression

Since C# 12, collection expressions give us the shortest form of all:

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

The spread element, .., copies every element of toppings into a brand new list.

What to notice is the target type written out in full. A collection expression has no type of its own, so it needs a type to convert to: write var here and the compiler answers with error CS9176: There is no target type for the collection expression.

We can also copy part of a list rather than all of it, with the GetRange method:

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

Here we pass the starting index and the number of elements we want, so passing 0 and toppings.Count copies the whole list into a new List<string>.

To make sure that everything we have done to copy a list of strings in C# works as intended, we can print all the results on the 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));

And check the result:

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

How Do We Deep Copy a List of Reference Types in C#?

With reference types the six forms above still work, and they no longer give us an independent list. Each produces a new list holding the same object references: two lists, one set of objects. Clear one pizza’s toppings through either list and both lists show it cleared. That is a shallow copy.

A deep copy is the alternative, and it is built one element at a time:

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

The work sits in the element type, not in the list. A copy constructor on Pizza decides what a copy of a pizza means, and it has to copy the Toppings list too, because that property is another reference.

So deep copying is recursive by nature. Every mutable object an element holds needs the same treatment, all the way down, which is why no single method in the framework can do it for us.

Shallow is cheaper and is usually enough. Choose deep when the objects have to diverge.

Building on the pizza trend from earlier, let’s expand on our example:

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)}";
    }
}

In a separate file, we declare a Pizza class with two simple properties, Name and Toppings, both required so that no pizza can be built without them. We also override the ToString method for easy visualization.

We are cloning lists, so we need one with reference types:

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"
        }
    }
};

Here we declare a pizzas variable of type List<Pizza> and add two pizzas to it.

Now, let’s look into the two types of copying we have when dealing with reference types.

Shallow Copy

We can easily achieve a shallow copy of our pizzas list with any of the methods from the previous section. But a shallow copy of a List<T>, where T is a reference type, copies only the structure of the collection and references to its elements, not the elements themselves. This means that changes to the elements in any of the two lists will be reflected in both the original and copied lists.

Let’s illustrate this:

var clonedPizzas = pizzas.ToList();

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

margherita.Toppings.Clear();

First, we create a clonedPizzas variable and clone the contents of pizzas to it using the ToList method (using any of the other methods used earlier will produce the same result). Then we get the Margherita pizza from the original list using the First method. Finally, we use the Clear method to empty the list of Toppings for that pizza.

Now, let’s see what happens:

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

We print the first pizza of each list to the console, which in both cases is the Margherita, using the First method.

We overrode the ToString method which will give us an easy-to-read representation of each pizza. Now, we can check the result:

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

We can see that both the original and copied Margherita pizzas now have an empty list of Toppings. This is because when creating a shallow copy, we only clone the references to the objects, not the actual objects. This is not ideal because when we change elements in one list, we change the elements everywhere we have copied that list.

This might cause serious problems for us, so let’s see what we can do to prevent it.

Before we move on, we put the Margherita’s toppings back, so that the deep copies we make next start from the full list:

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

What a Deep Copy Changes

The alternative is to create a deep copy – this means that we don’t just copy the references to the objects but create new, copied objects. This produces a different result than shallow copies as the objects referenced by the copied list are separate from those referenced in the original list.

There is no framework method that does it for us, so both techniques below build the new list one element at a time, either as a foreach loop or as the one-line projection [.. pizzas.Select(p => new Pizza(p))]. What differs between them is only how a single element is copied.

Records are worth a word here, because they look like the answer and are not: a with expression copies the references, not the objects, so a record holding a List<string> still shares that list with the instance it was copied from.

Deep Copy Using a Copy Constructor

A copy constructor is a constructor that takes an instance of the type as a parameter. Then the values of each property of that object are copied over to the newly created instance of that type:

public Pizza() { }

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

In our Pizza class, we create a new constructor that takes in a Pizza as a parameter. In it, we assign the Name and Toppings properties the values of the passed-in object, keeping in mind that we need to pass a copy of the Toppings, done with pizza.Toppings.ToList(), and not just assign the value.

The [SetsRequiredMembers] attribute, from the System.Diagnostics.CodeAnalysis namespace, is there because the two properties are required. It tells the compiler that this constructor sets both of them, so callers do not have to repeat them in an object initializer. Declaring a constructor also means C# no longer provides a parameterless one, which our object initializers still need, so we add an empty Pizza() constructor as well.

Now, we can move back to our Program class and do the cloning:

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

We project every pizza in the list through the copy constructor and collect the results into a new list with a collection expression. That is the whole deep copy: a new list, and a new Pizza for every element in it.

Deep Copy Using the ICloneable Interface

We can also use the Clone method we write when implementing the ICloneable interface to create a deep copy. It buys us a conventional name for the operation the copy constructor already performs, which is worth having in code that already uses the interface, and is the reason to keep reading rather than to start here: as the section at the end of this article shows, Microsoft recommends against implementing it in public APIs.

Let’s do the necessary updates:

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)}";
    }
}

We expand our Pizza class by implementing ICloneable and its Clone method. This time we create our implementation where we return a new Pizza object and copy the properties of the current instance – we assign Name directly as it is a string and then we use the ToList method we learned earlier to get a clone of the Toppings list.

Then we get on with the cloning:

var pizzasClonedWithICloneable = new List<Pizza>();

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

To do this we create a pizzasClonedWithICloneable variable as an empty List<Pizza>. Then, in a foreach loop iterating over our initial pizzas list, we get a new copy of the current pizza using the Clone method and add it to our new list.

Now, that we have used our two methods of creating a deep copy, let’s test them:

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()}");

After the foreach loop in our Program class, we again get the Margherita pizza and clear its list of Toppings. Then we print our Margherita pizzas on the console and examine the result:

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

This time around we can see that our task to create a deep copy when trying to clone a List was successful and the cloned pizzas have their Toppings intact as we have indeed created a deep copy with both approaches.

Which Way of Copying a List Should We Use?

Start from one question. Do the two lists need to hold different objects, or only to be different lists?

If only the lists differ, take a shallow copy and write it as a collection expression: List<string> clone = [.. toppings];. The List<T> constructor and ToList() give the identical result, so choose those when the source is a LINQ query or when the project is on an older language version.

If the objects have to differ too, the copy belongs to the element type. Give that type a copy constructor and project the list through it.

ICloneable is the one to leave alone in new code. Microsoft’s own guidance is blunt: because callers of Clone() cannot depend on the method performing a predictable cloning operation, it recommends that ICloneable not be implemented in public APIs. It does not say whether a Clone() is deep or shallow, so nothing that calls it can know either.

The guidance is quoted rather than paraphrased, from the interface’s own documentation: “Because callers of Clone() cannot depend on the method performing a predictable cloning operation, we recommend that ICloneable not be implemented in public APIs.”

All of this is about lists. For a deep copy of a single object, including the MemberwiseClone method and the serialization and reflection approaches, that is the article to read next.

The difference is easier to see than to describe: count the arrows arriving at each pizza.

Two panels. In the shallow copy panel, a box labelled pizzas and a box labelled clone both have arrows landing on the same two pizza objects, Margherita and Diavola, so every object takes two arrows. In the deep copy panel, pizzas points at the original Margherita and Diavola and clone points at two separate new objects of the same names, drawn in a different colour.

Here are the nine ways this article has shown, with the two questions that decide between them: does the approach hand us a new list, and does it hand us new elements?

ApproachCodeNew list?New elements?Reach for it when
Collection expressionList<string> copy = [.. toppings];YesNoThis is the default for a shallow copy
List<T> constructornew List<string>(toppings)YesNoThe same result on any language version
ToList()toppings.ToList()YesNoThe source is a LINQ query or an IEnumerable<T>
AddRange()copy.AddRange(toppings)No, fills a list we madeNoAdding to a list that already exists
GetRange() or toppings[..]toppings.GetRange(0, toppings.Count)YesNoCopying part of the list, not all of it
CopyTo()toppings.CopyTo(array)No, fills an array we sizedNoThe target has to be an array
ConvertAll()toppings.ConvertAll(t => t)YesOnly if the converter builds themChanging the element type while copying
Copy constructor, projected[.. pizzas.Select(p => new Pizza(p))]YesYesA deep copy, and the element type is ours
ICloneable.Clone(), projected[.. pizzas.Select(p => (Pizza)p.Clone())]YesOnly if Clone() is written toExisting code already implements it

Conclusion

In this article, we learned all about how to copy and clone a List in C#. We also learned that shallow copies are easy to achieve in many ways and work wonders with value types but may cause headaches when used with reference types. Deep copies are very useful but tend to be more expensive than shallow ones, due to the need for additional object creation. It can also be very complicated to achieve if we deal with very complex objects.

If we take one line away, let it be this one: a collection expression copies the list, and a copy constructor on the element type copies what is in it.

Tested with .NET 10.0.10.