Problemas de Copia en List<T> de C# con Tipos de Valor y Referencia

Al trabajar con List<T> en C#, es común enfrentar problemas de copia, especialmente en entornos multihilo, donde los datos pueden modificarse involuntariamente. Comprender el comportamiento de los tipos de valor y referencia es esencial para manejar correctamente las copias de listas.

Los tipos de valor almacenan directamente su contenido, mientras que los tipos de referencia mantienen una dirección de memoria que apunta a los datos. Por ejemplo, al asignar una variable de tipo valor a otra, se copia el valor real; las operaciones posteriores en una variable no afectan a la otra. En cambio, con tipos de referencia, la asignación copia la dirección, por lo que ambas variables apuntan al mismo objeto en memoria.

Para ilustrar, consideremos un ejemplo con variables de tipo valor:


int cantidadA = 0;
Console.WriteLine($"cantidadA: {cantidadA}");
int cantidadB = cantidadA;
Console.WriteLine($"cantidadA: {cantidadA}, cantidadB: {cantidadB}");
cantidadA = 1;
Console.WriteLine($"cantidadA: {cantidadA}, cantidadB: {cantidadB}");
cantidadB = 2;
Console.WriteLine($"cantidadA: {cantidadA}, cantidadB: {cantidadB}");

La salida muestra que los cambios en una variable no impactan en la otra, ya que se manipulan valores independientes.

Ahora, con un List<T>, que es un tipo de referencia, la asignación directa crea una referencia compartida:


List<int> coleccionX = new List<int>();
coleccionX.Add(10);
Console.WriteLine($"coleccionX: {string.Join(", ", coleccionX)}");
List<int> coleccionY = coleccionX;
Console.WriteLine($"coleccionX: {string.Join(", ", coleccionX)}, coleccionY: {string.Join(", ", coleccionY)}");
coleccionX.Add(20);
Console.WriteLine($"coleccionX: {string.Join(", ", coleccionX)}, coleccionY: {string.Join(", ", coleccionY)}");
coleccionY.Add(30);
Console.WriteLine($"coleccionX: {string.Join(", ", coleccionX)}, coleccionY: {string.Join(", ", coleccionY)}");

Ambas listas reflejan los mismos cambios, pues apuntan a la misma ubicación en memoria.

Para evitar este comportamiento, se deben utilizar copias profundas, que asignan nuevas direcciones de memoria a los objetos copiados. Existen varias técnicas, como la serialización con JSON, la reflexión o la serialización binaria.

Un método común emplea la serialización JSON con Newtonsoft.Json:


public static List<T> DuplicarViaJson<T>(this List<T> origen) where T : new()
{
    string representacion = JsonConvert.SerializeObject(origen);
    return JsonConvert.DeserializeObject<List<T>>(representacion);
}

Otro enfoque utiliza reflexión para copiar propiedades:


public static List<T> DuplicarViaPropiedades<T>(this List<T> origen) where T : new()
{
    List<T> destino = new List<T>();
    foreach (var elemento in origen)
    {
        T nuevoElemento = new T();
        var campos = typeof(T).GetProperties();
        foreach (var campo in campos)
        {
            object valor = campo.GetValue(elemento);
            campo.SetValue(nuevoElemento, valor);
        }
        destino.Add(nuevoElemento);
    }
    return destino;
}

También se puede usar la serialización binaria, requiriendo que la clase implemente el atributo [Serializable]:


public static List<T> DuplicarViaSerializacionBinaria<T>(this List<T> origen)
{
    using (MemoryStream flujo = new MemoryStream())
    {
        IFormatter formateador = new BinaryFormatter();
        formateador.Serialize(flujo, origen);
        flujo.Seek(0, SeekOrigin.Begin);
        return (List<T>)formateador.Deserialize(flujo);
    }
}

Para demostrar, definimos una clase de prueba y aplicamos los métodos:


[Serializable]
public class EntidadPrueba
{
    public int Identificador { get; set; }
    public byte[] Datos { get; set; }
    public string Descripcion { get; set; }

    public EntidadPrueba()
    {
        Identificador = 0;
        Datos = new byte[] { 0 };
        Descripcion = "";
    }
}

List<EntidadPrueba> listaOriginal = new List<EntidadPrueba>();
EntidadPrueba item1 = new EntidadPrueba { Identificador = 1, Datos = new byte[] { 1, 2 }, Descripcion = "Uno" };
listaOriginal.Add(item1);

List<EntidadPrueba> listaReferencia = listaOriginal; // Copia superficial
List<EntidadPrueba> listaCopiaJson = listaOriginal.DuplicarViaJson();
List<EntidadPrueba> listaCopiaPropiedades = listaOriginal.DuplicarViaPropiedades();
List<EntidadPrueba> listaCopiaBinaria = listaOriginal.DuplicarViaSerializacionBinaria();

// Modificaciones en listaOriginal afectan a listaReferencia, pero no a las copias profundas.
listaOriginal[0].Datos = new byte[] { 9, 9 };
listaOriginal[0].Descripcion = "Modificado";

// Las copias profundas mantienen datos independientes, excepto la serialización binaria en algunos casos.
listaCopiaJson[0].Datos = new byte[] { 8, 8 };
Console.WriteLine($"listaCopiaJson[0].Datos: {string.Join(", ", listaCopiaJson[0].Datos)}"); // Muestra 8,8
Console.WriteLine($"listaOriginal[0].Datos: {string.Join(", ", listaOriginal[0].Datos)}"); // Muestra 9,9

Los métodos de copia profunda mediante JSON y reflexión generan nuevas instancias, aislando los cambios. La serialización binaria puede preservar referencias compartidas en estructuras complejas.

Etiquetas: C# List&lt;T&gt; tipos de valor tipos de referencia copia profunda

Publicado el 7-15 01:47