¿Cómo se hace una copia profunda de un objeto en .NET? [duplicar]

Resuelto user18931 asked hace 15 años • 10 respuestas

Quiero una copia verdadera y profunda. En Java, esto fue fácil, pero ¿cómo se hace en C#?

user18931 avatar Sep 25 '08 02:09 user18931
Aceptado

Nota IMPORTANTE

BinaryFormatter ha quedado obsoleto y ya no estará disponible en .NET después de noviembre de 2023. Consulte la estrategia de obsolescencia de BinaryFormatter.


He visto algunos enfoques diferentes para esto, pero uso un método de utilidad genérico como tal:

public static T DeepClone<T>(this T obj)
{
 using (var ms = new MemoryStream())
 {
   var formatter = new BinaryFormatter();
   formatter.Serialize(ms, obj);
   ms.Position = 0;

   return (T) formatter.Deserialize(ms);
 }
}

Notas:

  • Su clase DEBE estar marcada como tal [Serializable]para que esto funcione.

  • Su archivo fuente debe incluir el siguiente código:

     using System.Runtime.Serialization.Formatters.Binary;
     using System.IO;
    
Kilhoffer avatar Sep 24 '2008 19:09 Kilhoffer

Escribí un método de extensión de copia profunda de objetos , basado en el recursivo "MemberwiseClone" . Es rápido ( tres veces más rápido que BinaryFormatter) y funciona con cualquier objeto. No necesita un constructor predeterminado ni atributos serializables.

Código fuente:

using System.Collections.Generic;
using System.Reflection;
using System.ArrayExtensions;

namespace System
{
    public static class ObjectExtensions
    {
        private static readonly MethodInfo CloneMethod = typeof(Object).GetMethod("MemberwiseClone", BindingFlags.NonPublic | BindingFlags.Instance);

        public static bool IsPrimitive(this Type type)
        {
            if (type == typeof(String)) return true;
            return (type.IsValueType & type.IsPrimitive);
        }

        public static Object Copy(this Object originalObject)
        {
            return InternalCopy(originalObject, new Dictionary<Object, Object>(new ReferenceEqualityComparer()));
        }
        private static Object InternalCopy(Object originalObject, IDictionary<Object, Object> visited)
        {
            if (originalObject == null) return null;
            var typeToReflect = originalObject.GetType();
            if (IsPrimitive(typeToReflect)) return originalObject;
            if (visited.ContainsKey(originalObject)) return visited[originalObject];
            if (typeof(Delegate).IsAssignableFrom(typeToReflect)) return null;
            var cloneObject = CloneMethod.Invoke(originalObject, null);
            if (typeToReflect.IsArray)
            {
                var arrayType = typeToReflect.GetElementType();
                if (IsPrimitive(arrayType) == false)
                {
                    Array clonedArray = (Array)cloneObject;
                    clonedArray.ForEach((array, indices) => array.SetValue(InternalCopy(clonedArray.GetValue(indices), visited), indices));
                }

            }
            visited.Add(originalObject, cloneObject);
            CopyFields(originalObject, visited, cloneObject, typeToReflect);
            RecursiveCopyBaseTypePrivateFields(originalObject, visited, cloneObject, typeToReflect);
            return cloneObject;
        }

        private static void RecursiveCopyBaseTypePrivateFields(object originalObject, IDictionary<object, object> visited, object cloneObject, Type typeToReflect)
        {
            if (typeToReflect.BaseType != null)
            {
                RecursiveCopyBaseTypePrivateFields(originalObject, visited, cloneObject, typeToReflect.BaseType);
                CopyFields(originalObject, visited, cloneObject, typeToReflect.BaseType, BindingFlags.Instance | BindingFlags.NonPublic, info => info.IsPrivate);
            }
        }

        private static void CopyFields(object originalObject, IDictionary<object, object> visited, object cloneObject, Type typeToReflect, BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy, Func<FieldInfo, bool> filter = null)
        {
            foreach (FieldInfo fieldInfo in typeToReflect.GetFields(bindingFlags))
            {
                if (filter != null && filter(fieldInfo) == false) continue;
                if (IsPrimitive(fieldInfo.FieldType)) continue;
                var originalFieldValue = fieldInfo.GetValue(originalObject);
                var clonedFieldValue = InternalCopy(originalFieldValue, visited);
                fieldInfo.SetValue(cloneObject, clonedFieldValue);
            }
        }
        public static T Copy<T>(this T original)
        {
            return (T)Copy((Object)original);
        }
    }

    public class ReferenceEqualityComparer : EqualityComparer<Object>
    {
        public override bool Equals(object x, object y)
        {
            return ReferenceEquals(x, y);
        }
        public override int GetHashCode(object obj)
        {
            if (obj == null) return 0;
            return obj.GetHashCode();
        }
    }

    namespace ArrayExtensions
    {
        public static class ArrayExtensions
        {
            public static void ForEach(this Array array, Action<Array, int[]> action)
            {
                if (array.LongLength == 0) return;
                ArrayTraverse walker = new ArrayTraverse(array);
                do action(array, walker.Position);
                while (walker.Step());
            }
        }

        internal class ArrayTraverse
        {
            public int[] Position;
            private int[] maxLengths;

            public ArrayTraverse(Array array)
            {
                maxLengths = new int[array.Rank];
                for (int i = 0; i < array.Rank; ++i)
                {
                    maxLengths[i] = array.GetLength(i) - 1;
                }
                Position = new int[array.Rank];
            }

            public bool Step()
            {
                for (int i = 0; i < Position.Length; ++i)
                {
                    if (Position[i] < maxLengths[i])
                    {
                        Position[i]++;
                        for (int j = 0; j < i; j++)
                        {
                            Position[j] = 0;
                        }
                        return true;
                    }
                }
                return false;
            }
        }
    }

}
Alex Burtsev avatar Jul 03 '2012 10:07 Alex Burtsev

Aprovechando la solución de Kilhoffer...

Con C# 3.0 puedes crear un método de extensión de la siguiente manera:

public static class ExtensionMethods
{
    // Deep clone
    public static T DeepClone<T>(this T a)
    {
        using (MemoryStream stream = new MemoryStream())
        {
            BinaryFormatter formatter = new BinaryFormatter();
            formatter.Serialize(stream, a);
            stream.Position = 0;
            return (T) formatter.Deserialize(stream);
        }
    }
}

que extiende cualquier clase que haya sido marcada como [Serializable] con un método DeepClone

MyClass copy = obj.DeepClone();
Neil avatar Jul 31 '2009 16:07 Neil

Puede utilizar Nested MemberwiseClone para realizar una copia profunda . Es casi la misma velocidad que copiar una estructura de valor y es un orden de magnitud más rápido que (a) reflexión o (b) serialización (como se describe en otras respuestas en esta página).

Tenga en cuenta que si usa Nested MemberwiseClone para una copia profunda , debe implementar manualmente un ShallowCopy para cada nivel anidado en la clase y un DeepCopy que llame a todos los métodos ShallowCopy mencionados para crear un clon completo. Esto es simple: solo unas pocas líneas en total; consulte el código de demostración a continuación.

Aquí está el resultado del código que muestra la diferencia de rendimiento relativa (4,77 segundos para Nested MemberwiseClone profundo frente a 39,93 segundos para Serialización). Usar Nested MemberwiseClone es casi tan rápido como copiar una estructura, y copiar una estructura está bastante cerca de la velocidad máxima teórica de la que es capaz .NET, que probablemente esté bastante cerca de la velocidad de la misma cosa en C o C++ (pero sería tener que ejecutar algunos puntos de referencia equivalentes para comprobar esta afirmación).

    Demo of shallow and deep copy, using classes and MemberwiseClone:
      Create Bob
        Bob.Age=30, Bob.Purchase.Description=Lamborghini
      Clone Bob >> BobsSon
      Adjust BobsSon details
        BobsSon.Age=2, BobsSon.Purchase.Description=Toy car
      Proof of deep copy: If BobsSon is a true clone, then adjusting BobsSon details will not affect Bob:
        Bob.Age=30, Bob.Purchase.Description=Lamborghini
      Elapsed time: 00:00:04.7795670,30000000
    Demo of shallow and deep copy, using structs and value copying:
      Create Bob
        Bob.Age=30, Bob.Purchase.Description=Lamborghini
      Clone Bob >> BobsSon
      Adjust BobsSon details:
        BobsSon.Age=2, BobsSon.Purchase.Description=Toy car
      Proof of deep copy: If BobsSon is a true clone, then adjusting BobsSon details will not affect Bob:
        Bob.Age=30, Bob.Purchase.Description=Lamborghini
      Elapsed time: 00:00:01.0875454,30000000
    Demo of deep copy, using class and serialize/deserialize:
      Elapsed time: 00:00:39.9339425,30000000

Para entender cómo hacer una copia profunda usando Nested MemberwiseClone, aquí está el proyecto de demostración:

// Nested MemberwiseClone example. 
// Added to demo how to deep copy a reference class.
[Serializable] // Not required if using MemberwiseClone, only used for speed comparison using serialization.
public class Person
{
    public Person(int age, string description)
    {
        this.Age = age;
        this.Purchase.Description = description;
    }
    [Serializable] // Not required if using MemberwiseClone
    public class PurchaseType
    {
        public string Description;
        public PurchaseType ShallowCopy()
        {
            return (PurchaseType)this.MemberwiseClone();
        }
    }
    public PurchaseType Purchase = new PurchaseType();
    public int Age;
    // Add this if using nested MemberwiseClone.
    // This is a class, which is a reference type, so cloning is more difficult.
    public Person ShallowCopy()
    {
        return (Person)this.MemberwiseClone();
    }
    // Add this if using nested MemberwiseClone.
    // This is a class, which is a reference type, so cloning is more difficult.
    public Person DeepCopy()
    {
            // Clone the root ...
        Person other = (Person) this.MemberwiseClone();
            // ... then clone the nested class.
        other.Purchase = this.Purchase.ShallowCopy();
        return other;
    }
}
// Added to demo how to copy a value struct (this is easy - a deep copy happens by default)
public struct PersonStruct
{
    public PersonStruct(int age, string description)
    {
        this.Age = age;
        this.Purchase.Description = description;
    }
    public struct PurchaseType
    {
        public string Description;
    }
    public PurchaseType Purchase;
    public int Age;
    // This is a struct, which is a value type, so everything is a clone by default.
    public PersonStruct ShallowCopy()
    {
        return (PersonStruct)this;
    }
    // This is a struct, which is a value type, so everything is a clone by default.
    public PersonStruct DeepCopy()
    {
        return (PersonStruct)this;
    }
}
// Added only for a speed comparison.
public class MyDeepCopy
{
    public static T DeepCopy<T>(T obj)
    {
        object result = null;
        using (var ms = new MemoryStream())
        {
            var formatter = new BinaryFormatter();
            formatter.Serialize(ms, obj);
            ms.Position = 0;
            result = (T)formatter.Deserialize(ms);
            ms.Close();
        }
        return (T)result;
    }
}

Luego, llame a la demostración desde principal:

    void MyMain(string[] args)
    {
        {
            Console.Write("Demo of shallow and deep copy, using classes and Nested MemberwiseClone:\n");
            var Bob = new Person(30, "Lamborghini");
            Console.Write("  Create Bob\n");
            Console.Write("    Bob.Age={0}, Bob.Purchase.Description={1}\n", Bob.Age, Bob.Purchase.Description);
            Console.Write("  Clone Bob >> BobsSon\n");
            var BobsSon = Bob.DeepCopy();
            Console.Write("  Adjust BobsSon details\n");
            BobsSon.Age = 2;
            BobsSon.Purchase.Description = "Toy car";
            Console.Write("    BobsSon.Age={0}, BobsSon.Purchase.Description={1}\n", BobsSon.Age, BobsSon.Purchase.Description);
            Console.Write("  Proof of deep copy: If BobsSon is a true clone, then adjusting BobsSon details will not affect Bob:\n");
            Console.Write("    Bob.Age={0}, Bob.Purchase.Description={1}\n", Bob.Age, Bob.Purchase.Description);
            Debug.Assert(Bob.Age == 30);
            Debug.Assert(Bob.Purchase.Description == "Lamborghini");
            var sw = new Stopwatch();
            sw.Start();
            int total = 0;
            for (int i = 0; i < 100000; i++)
            {
                var n = Bob.DeepCopy();
                total += n.Age;
            }
            Console.Write("  Elapsed time: {0},{1}\n", sw.Elapsed, total);
        }
        {               
            Console.Write("Demo of shallow and deep copy, using structs:\n");
            var Bob = new PersonStruct(30, "Lamborghini");
            Console.Write("  Create Bob\n");
            Console.Write("    Bob.Age={0}, Bob.Purchase.Description={1}\n", Bob.Age, Bob.Purchase.Description);
            Console.Write("  Clone Bob >> BobsSon\n");
            var BobsSon = Bob.DeepCopy();
            Console.Write("  Adjust BobsSon details:\n");
            BobsSon.Age = 2;
            BobsSon.Purchase.Description = "Toy car";
            Console.Write("    BobsSon.Age={0}, BobsSon.Purchase.Description={1}\n", BobsSon.Age, BobsSon.Purchase.Description);
            Console.Write("  Proof of deep copy: If BobsSon is a true clone, then adjusting BobsSon details will not affect Bob:\n");
            Console.Write("    Bob.Age={0}, Bob.Purchase.Description={1}\n", Bob.Age, Bob.Purchase.Description);                
            Debug.Assert(Bob.Age == 30);
            Debug.Assert(Bob.Purchase.Description == "Lamborghini");
            var sw = new Stopwatch();
            sw.Start();
            int total = 0;
            for (int i = 0; i < 100000; i++)
            {
                var n = Bob.DeepCopy();
                total += n.Age;
            }
            Console.Write("  Elapsed time: {0},{1}\n", sw.Elapsed, total);
        }
        {
            Console.Write("Demo of deep copy, using class and serialize/deserialize:\n");
            int total = 0;
            var sw = new Stopwatch();
            sw.Start();
            var Bob = new Person(30, "Lamborghini");
            for (int i = 0; i < 100000; i++)
            {
                var BobsSon = MyDeepCopy.DeepCopy<Person>(Bob);
                total += BobsSon.Age;
            }
            Console.Write("  Elapsed time: {0},{1}\n", sw.Elapsed, total);
        }
        Console.ReadKey();
    }

Nuevamente, tenga en cuenta que si usa Nested MemberwiseClone para una copia profunda , debe implementar manualmente un ShallowCopy para cada nivel anidado en la clase y un DeepCopy que llame a todos los métodos ShallowCopy mencionados para crear un clon completo. Esto es simple: solo unas pocas líneas en total; consulte el código de demostración anterior.

Tenga en cuenta que cuando se trata de clonar un objeto, existe una gran diferencia entre una "estructura" y una "clase":

  • Si tiene una "estructura", es un tipo de valor, por lo que puede copiarlo y el contenido se clonará.
  • Si tiene una "clase", es un tipo de referencia, por lo que si la copia, todo lo que hará es copiar el puntero a ella. Para crear un clon verdadero, debes ser más creativo y utilizar un método que cree otra copia del objeto original en la memoria.
  • La clonación incorrecta de objetos puede provocar errores muy difíciles de localizar. En el código de producción, tiendo a implementar una suma de verificación para verificar que el objeto se haya clonado correctamente y que no haya sido dañado por otra referencia al mismo. Esta suma de comprobación se puede desactivar en el modo Release.
  • Este método me parece bastante útil: a menudo, sólo deseas clonar partes del objeto, no todo. También es esencial para cualquier caso de uso en el que se modifiquen objetos y luego se introduzcan las copias modificadas en una cola.

Actualizar

Probablemente sea posible utilizar la reflexión para recorrer de forma recursiva el gráfico de objetos y realizar una copia profunda. WCF utiliza esta técnica para serializar un objeto, incluidos todos sus hijos. El truco consiste en anotar todos los objetos secundarios con un atributo que los haga reconocibles. Sin embargo, es posible que pierda algunos beneficios de rendimiento.

Actualizar

Cita sobre prueba de velocidad independiente (ver comentarios a continuación):

Realicé mi propia prueba de velocidad utilizando el método de extensión serializar/deserializar de Neil, Nested MemberwiseClone de Contango, el método de extensión basado en reflexión de Alex Burtsev y AutoMapper, 1 millón de veces cada uno. Serializar-deserializar fue el más lento: tardó 15,7 segundos. Luego vino AutoMapper, que tardó 10,1 segundos. Mucho más rápido fue el método basado en la reflexión, que tardó 2,4 segundos. Con diferencia, el más rápido fue Nested MemberwiseClone, con una duración de 0,1 segundos. Todo se reduce al rendimiento versus la molestia de agregar código a cada clase para clonarla. Si el rendimiento no es un problema, opte por el método de Alex Burtsev. – Simón Tewsi

Contango avatar Dec 30 '2011 19:12 Contango