Para copiar una List<T> en C# basta con una sola expresión: List<string> copy = [.. original]; nos da una lista nueva con los mismos elementos. El constructor de List<T> y el método ToList() de LINQ hacen exactamente lo mismo.

Para descargar el código fuente de este artículo, puedes visitar nuestro repositorio de GitHub.

El problema está en qué significa “los mismos elementos”. Si los elementos son tipos de valor que no contienen referencias, o tipos inmutables como string, la copia es independiente y no hay nada más en que pensar. Si son objetos, ambas listas apuntan a los mismos objetos, así que modificar uno de esos objetos lo modifica en las dos listas.

Ese segundo caso es el que vale la pena leer. Obtener una lista cuyos objetos también son copias se llama copia profunda, y C# no tiene un único método para ello, porque lo que significa “una copia” depende del tipo que contiene la lista.

¿Cómo copiar una lista de tipos de valor en C#?

Para copiar una lista de tipos de valor basta con una sola expresión. List<string> clone = [.. toppings]; crea una lista nueva y copia en ella todos los elementos.

Otras cinco formas hacen el mismo trabajo. El constructor de List<T> acepta cualquier IEnumerable<T>, el método ToList() de LINQ lee cualquier secuencia, AddRange() llena una lista que ya existe, GetRange() copia una porción de una lista y ConvertAll() copia mientras cambia el tipo de los elementos.

Todas ellas copian los propios elementos, porque en eso consiste un tipo de valor: la variable contiene el valor y no una referencia a él. Si modificas el clon, el original no cambia.

string es la excepción que se comporta como la regla. Es un tipo de referencia, así que una lista copiada comparte los objetos de cadena, y es inmutable, así que ningún código puede modificar el texto original a través de la copia.

CopyTo() es el que no encaja en el grupo. Llena un array que ya hemos dimensionado, así que recurre a él solo cuando el destino tenga que ser un array.

Todos los ejemplos de esta sección copian la misma List<T> de ingredientes de pizza, y la distinción en la que se basan es la que hay entre tipos de valor y tipos de referencia. Esta es la lista:

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

Aquí, definimos una variable toppings de tipo List<string> que contiene algunos de los ingredientes de la clásica pizza Margherita.

A continuación, veremos las opciones para clonar los valores de esa List<string> en otra.

Con el constructor de la lista

Una de las sobrecargas del constructor de List<T> recibe un IEnumerable<T>:

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

Aquí, inicializamos una nueva variable toppingsClonedWithConstructor que contiene los elementos copiados de la lista toppings.

Con el método CopyTo de la lista

List<T> tiene varios métodos que podemos usar para clonar su contenido en otra lista o colección.

Uno de ellos es el método CopyTo. La sobrecarga de un solo argumento que llamamos aquí es propia de la lista, y podemos usarla para clonar la List<T> en un T[]:

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

Primero, inicializamos una variable toppingsClonedWithCopyTo como un string[] cuya longitud es igual a la de la lista toppings, de ahí el uso de toppings.Count. Después usamos el método CopyTo de la lista toppings y le pasamos como parámetro el array recién inicializado. Así, su contenido se copia en toppingsClonedWithCopyTo.

Si el destino es un array que ya tenemos, es el mismo trabajo que copiar elementos en un array, y el array tiene que estar dimensionado antes de la llamada.

Con el método AddRange de la lista

Otro método que recibe un IEnumerable<T> como parámetro es el método AddRange:

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

El método AddRange necesita una List<T> ya inicializada, así que declaramos la variable toppingsClonedWithAddRange y le asignamos una List<string> vacía. Después pasamos toppings como parámetro al método AddRange, y este clona el contenido por nosotros.

Con el método ToList de Enumerable

El espacio de nombres System.Linq nos proporciona el método Enumerable.ToList:

var toppingsClonedWithToList = toppings.ToList();

Aquí, inicializamos directamente la variable toppingsClonedWithToList llamando al método ToList sobre la variable toppings que ya existe, lo que clona su contenido en la nueva lista.

Como alternativa, podemos usar de la misma forma el método ToArray de la propia lista si queremos clonar el contenido de una List<T> en T[].

Con el método ConvertAll

List<T> tiene otro método útil que a primera vista puede parecer algo intimidante: el método ConvertAll<TOutput>(Converter<T, TOutput>). Convierte todos los elementos de una lista de un tipo a otro y devuelve una lista con los elementos convertidos. Incluso podemos usarlo para clonar:

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

Primero inicializamos una variable toppingsClonedWithConvertAll y le asignamos el valor que devuelve el método ConvertAll aplicado a la lista toppings. El método recibe un Converter<TInput, TOutput>, que no es más que un delegado de un método que convierte un elemento de un tipo a otro. Recibe el nombre del método que se usa para la conversión, aunque también podemos pasar un método anónimo.

No necesitamos un método aparte para convertir el elemento que pasamos, así que usamos simplemente una expresión lambda que devuelve el mismo valor, de ahí el uso de x => x.

Con una expresión de colección

Desde C# 12, las expresiones de colección nos ofrecen la forma más corta de todas:

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

El elemento de propagación (spread), .., copia todos los elementos de toppings en una lista completamente nueva.

Lo que hay que observar es que el tipo de destino está escrito de forma explícita. Una expresión de colección no tiene tipo propio, así que necesita un tipo al que convertirse: escribe var aquí y el compilador responde con error CS9176: There is no target type for the collection expression..

The Web API Production Checklist, ebook gratuito en inglés

Ebook gratis

¿Tu Web API está lista para producción?

33 puntos que comprobar antes de desplegarla, con la solución de cada uno. Un PDF gratuito de 76 páginas para .NET 10.

El ebook está en inglés.

Descarga la checklist gratis

PDF gratuito. Un solo correo para enviártelo. Puedes darte de baja cuando quieras.

También podemos copiar solo una parte de una lista, en lugar de toda, con el método GetRange:

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

Aquí pasamos el índice inicial y el número de elementos que queremos, así que pasar 0 y toppings.Count copia toda la lista en una nueva List<string>.

Para asegurarnos de que todo lo que hemos hecho para copiar una lista de cadenas en C# funciona como esperamos, podemos imprimir todos los resultados en la consola:

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

Y comprobar el 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

¿Cómo hacer una copia profunda de una lista de tipos de referencia en C#?

Con tipos de referencia, las seis formas anteriores siguen funcionando, pero ya no nos dan una lista independiente. Cada una produce una lista nueva que contiene las mismas referencias a objetos: dos listas, un único conjunto de objetos. Si vacías los ingredientes de una pizza a través de cualquiera de las dos listas, ambas listas muestran esa pizza sin ingredientes. Eso es una copia superficial.

La alternativa es una copia profunda, y se construye elemento a elemento:

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

El trabajo recae en el tipo de los elementos, no en la lista. Un constructor de copia en Pizza decide qué significa una copia de una pizza, y también tiene que copiar la lista Toppings, porque esa propiedad es otra referencia.

Así que la copia profunda es recursiva por naturaleza. Cada objeto mutable que hay dentro de un elemento necesita el mismo tratamiento, hasta el último nivel, y por eso no hay en el framework un único método que pueda hacerlo por nosotros.

La copia superficial es más barata y suele bastar. Elige la profunda cuando los objetos tengan que divergir.

Siguiendo con la temática de pizzas de antes, vamos a ampliar el ejemplo:

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

En un archivo aparte, declaramos una clase Pizza con dos propiedades sencillas, Name y Toppings, ambas required para que no se pueda crear ninguna pizza sin ellas. También sobrescribimos (override) el método ToString para visualizarla fácilmente.

Estamos clonando listas, así que necesitamos una con tipos de referencia:

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

Aquí declaramos una variable pizzas de tipo List<Pizza> y le agregamos dos pizzas.

Ahora, veamos los dos tipos de copia que existen cuando trabajamos con tipos de referencia.

Copia superficial

Podemos obtener fácilmente una copia superficial de la lista pizzas con cualquiera de los métodos de la sección anterior. Pero una copia superficial de una List<T>, donde T es un tipo de referencia, copia solo la estructura de la colección y las referencias a sus elementos, no los elementos en sí. Esto significa que los cambios en los elementos de cualquiera de las dos listas se reflejarán tanto en la lista original como en la copiada.

Vamos a ilustrarlo:

var clonedPizzas = pizzas.ToList();

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

margherita.Toppings.Clear();

Primero, creamos una variable clonedPizzas y clonamos en ella el contenido de pizzas con el método ToList (cualquiera de los otros métodos que usamos antes produce el mismo resultado). Después obtenemos la pizza Margherita de la lista original con el método First. Por último, usamos el método Clear para vaciar la lista Toppings de esa pizza.

Ahora, veamos qué ocurre:

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

Con el método First, imprimimos en la consola la primera pizza de cada lista, que en ambos casos es la Margherita.

Hemos sobrescrito el método ToString, que nos da una representación fácil de leer de cada pizza. Ahora podemos comprobar el resultado:

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

Vemos que tanto la pizza Margherita original como la copiada tienen ahora vacía la lista Toppings. Esto se debe a que, al crear una copia superficial, solo clonamos las referencias a los objetos, no los objetos en sí. No es lo ideal, porque cuando modificamos elementos de una lista, modificamos esos elementos en todos los lugares donde hayamos copiado esa lista.

Esto podría causarnos problemas graves, así que veamos qué podemos hacer para evitarlo.

Antes de seguir, volvemos a poner los ingredientes de la pizza Margherita, para que las copias profundas que hacemos a continuación partan de la lista completa:

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

Qué cambia con una copia profunda

La alternativa es crear una copia profunda, lo que significa que no solo copiamos las referencias a los objetos, sino que creamos copias nuevas de los objetos. El resultado es distinto al de las copias superficiales, ya que los objetos a los que hace referencia la lista copiada son independientes de los que referencia la lista original.

No hay ningún método del framework que lo haga por nosotros, así que las dos técnicas siguientes construyen la lista nueva elemento a elemento, ya sea con un bucle foreach o con la proyección [.. pizzas.Select(p => new Pizza(p))] de una sola línea. Lo único que cambia entre ellas es cómo se copia cada elemento.

Vale la pena mencionar los records, porque parecen la solución, pero no lo son: una expresión with copia las referencias, no los objetos, así que un record que contiene una List<string> sigue compartiendo esa lista con la instancia de la que se copió.

The Web API Production Checklist, ebook gratuito en inglés

Ebook gratis

¿Tu Web API está lista para producción?

33 puntos que comprobar antes de desplegarla, con la solución de cada uno. Un PDF gratuito de 76 páginas para .NET 10.

El ebook está en inglés.

Descarga la checklist gratis

PDF gratuito. Un solo correo para enviártelo. Puedes darte de baja cuando quieras.

Copia profunda con un constructor de copia

Un constructor de copia es un constructor que recibe como parámetro una instancia de su propio tipo. Después, los valores de cada propiedad de ese objeto se copian en la nueva instancia de ese tipo:

public Pizza() { }

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

En la clase Pizza, creamos un nuevo constructor que recibe un Pizza como parámetro. En él, asignamos a las propiedades Name y Toppings los valores del objeto recibido, teniendo en cuenta que tenemos que pasar una copia de Toppings, que obtenemos con pizza.Toppings.ToList(), y no limitarnos a asignar el valor.

El atributo [SetsRequiredMembers], del espacio de nombres System.Diagnostics.CodeAnalysis, está ahí porque las dos propiedades son required. Le indica al compilador que este constructor establece ambas, así que quien lo llame no tiene que repetirlas en un inicializador de objeto. Declarar un constructor implica, además, que C# deja de proporcionar uno sin parámetros, que nuestros inicializadores de objeto siguen necesitando, así que también agregamos un constructor Pizza() vacío.

Ahora podemos volver a la clase Program y hacer la clonación:

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

Proyectamos cada pizza de la lista a través del constructor de copia y reunimos los resultados en una lista nueva con una expresión de colección. Esa es toda la copia profunda: una lista nueva y un Pizza nuevo por cada uno de sus elementos.

Copia profunda con la interfaz ICloneable

También podemos usar el método Clone que escribimos al implementar la interfaz ICloneable para crear una copia profunda. Nos da un nombre convencional para la misma operación que ya realiza el constructor de copia, algo útil en código que ya usa la interfaz. Por eso vale la pena seguir leyendo, pero no es por donde conviene empezar: como muestra el apartado final de este artículo, Microsoft recomienda no implementarla en API públicas.

Vamos a hacer las actualizaciones necesarias:

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 la clase Pizza para que implemente la interfaz ICloneable y su método Clone. Esta vez creamos nuestra propia implementación, en la que devolvemos un nuevo objeto Pizza y copiamos las propiedades de la instancia actual: asignamos Name directamente, ya que es un string, y después usamos el método ToList que vimos antes para obtener un clon de la lista Toppings.

Después pasamos a la clonación:

var pizzasClonedWithICloneable = new List<Pizza>();

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

Para ello, creamos una variable pizzasClonedWithICloneable como una List<Pizza> vacía. Después, en un bucle foreach que itera sobre la lista inicial pizzas, obtenemos una nueva copia de la pizza actual con el método Clone y la agregamos a la lista nueva.

Ahora que hemos usado nuestros dos métodos para crear una copia profunda, vamos a probarlos:

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

Después del bucle foreach de la clase Program, volvemos a obtener la pizza Margherita y vaciamos su lista Toppings. A continuación, imprimimos las pizzas Margherita en la consola y examinamos el 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

Esta vez vemos que nuestro objetivo de crear una copia profunda al clonar una List se ha cumplido, y las pizzas clonadas conservan intacta la lista Toppings, ya que con ambos enfoques hemos creado realmente una copia profunda.

¿Qué forma de copiar una lista conviene usar?

Empieza por una pregunta. ¿Las dos listas tienen que contener objetos distintos o solo ser listas distintas?

Si solo tienen que ser listas distintas, haz una copia superficial y escríbela como una expresión de colección: List<string> clone = [.. toppings];. El constructor de List<T> y ToList() dan un resultado idéntico, así que elígelos cuando el origen sea una consulta LINQ o cuando el proyecto use una versión anterior del lenguaje.

Si los objetos también tienen que ser distintos, la copia es responsabilidad del tipo de los elementos. Dale a ese tipo un constructor de copia y proyecta la lista a través de él.

ICloneable es la opción que conviene evitar en código nuevo. La recomendación de Microsoft es tajante: como quienes llaman a Clone() no pueden confiar en que el método realice una operación de clonación predecible, recomienda no implementar ICloneable en API públicas. La interfaz no indica si Clone() hace una copia profunda o superficial, así que ningún código que lo llame puede saberlo tampoco.

La recomendación se cita textualmente, sin parafrasear, de la propia documentación de la interfaz: “Como quienes llaman a Clone() no pueden confiar en que el método realice una operación de clonación predecible, recomendamos no implementar ICloneable en API públicas”.

Todo esto trata sobre listas. Para una copia profunda de un solo objeto, incluidos el método MemberwiseClone y los enfoques basados en serialización y reflexión, ese es el artículo que conviene leer a continuación.

La diferencia es más fácil de ver que de describir: cuenta las flechas que llegan a cada pizza.

Dos paneles. En el panel de la copia superficial, un cuadro con la etiqueta pizzas y otro con la etiqueta clone tienen flechas que llegan a los mismos dos objetos pizza, Margherita y Diavola, así que a cada objeto le llegan dos flechas. En el panel de la copia profunda, pizzas apunta a las Margherita y Diavola originales, y clone apunta a dos objetos nuevos e independientes con los mismos nombres, dibujados en otro color.

Estas son las nueve formas que ha mostrado este artículo, con las dos preguntas que deciden entre ellas: ¿el enfoque nos da una lista nueva?, ¿nos da elementos nuevos?

EnfoqueCódigo¿Lista nueva?¿Elementos nuevos?Úsalo cuando
Expresión de colecciónList<string> copy = [.. toppings];SíNoEs la opción predeterminada para una copia superficial
Constructor de List<T>new List<string>(toppings)SíNoEl mismo resultado en cualquier versión del lenguaje
ToList()toppings.ToList()SíNoEl origen es una consulta LINQ o un IEnumerable<T>
AddRange()copy.AddRange(toppings)No, llena una lista que creamosNoAgregar elementos a una lista que ya existe
GetRange() o toppings[..]toppings.GetRange(0, toppings.Count)SíNoCopiar una parte de la lista, no toda
CopyTo()toppings.CopyTo(array)No, llena un array que dimensionamosNoEl destino tiene que ser un array
ConvertAll()toppings.ConvertAll(t => t)SíSolo si el conversor los creaCambiar el tipo de los elementos durante la copia
Constructor de copia, con proyección[.. pizzas.Select(p => new Pizza(p))]SíSíUna copia profunda, y el tipo de los elementos es nuestro
ICloneable.Clone(), con proyección[.. pizzas.Select(p => (Pizza)p.Clone())]SíSolo si Clone() está escrito para crearlosEl código existente ya la implementa

Conclusión

En este artículo, hemos aprendido todo sobre cómo copiar y clonar una List en C#. También hemos aprendido que las copias superficiales son fáciles de conseguir de muchas formas y funcionan de maravilla con tipos de valor, pero pueden dar dolores de cabeza con tipos de referencia. Las copias profundas son muy útiles, pero suelen ser más costosas que las superficiales, porque hay que crear objetos adicionales. Además, pueden ser muy complicadas de conseguir si trabajamos con objetos muy complejos.

Si nos quedamos con una sola idea, que sea esta: una expresión de colección copia la lista, y un constructor de copia en el tipo de los elementos copia lo que contiene.

Probado con .NET 10.0.10.