web-dev-qa-db-fra.com

Comment faites-vous une copie complète d'un objet dans .NET (C # spécifiquement)?

Je veux une vraie copie profonde. En Java, c'était facile, mais comment le faire en C #?

548
user18931

J'ai déjà vu différentes approches, mais j'utilise une méthode utilitaire générique en tant que telle:

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

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

Remarques:

  • Votre classe DOIT être marquée [Serializable] pour que cela fonctionne.
  • Votre fichier source doit inclure le code suivant:

    using System.Runtime.Serialization.Formatters.Binary;
    using System.IO;
    
596
Kilhoffer

I a écrit une méthode d'extension de copie d'objet profond , basé sur la méthode récursive "MemberwiseClone" . Il est rapide ( trois fois plus rapide que BinaryFormatter) et fonctionne avec n'importe quel objet. Vous n'avez pas besoin d'un constructeur par défaut ou d'attributs sérialisables.

Code source:

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

}
294
Alex Burtsev

S'appuyant sur la solution de Kilhoffer ...

Avec C # 3.0, vous pouvez créer une méthode d’extension comme suit:

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

qui étend toute classe qui a été marquée comme [Serializable] avec une méthode DeepClone

MyClass copy = obj.DeepClone();
159
Neil

Vous pouvez utiliser Nested MemberwiseClone pour faire une copie complète. C'est presque la même vitesse que de copier une structure de valeur, et c'est un ordre de grandeur plus rapide que (a) la réflexion ou (b) la sérialisation (comme décrit dans d'autres réponses sur cette page).

Notez que if vous utilisez Nested MemberwiseClone pour une copie complète, vous devez implémenter manuellement un ShallowCopy pour chaque niveau imbriqué de la classe et un DeepCopy qui appelle toutes les méthodes dites ShallowCopy. pour créer un clone complet. C'est simple: quelques lignes au total, voir le code de démonstration ci-dessous.

Voici la sortie du code indiquant la différence de performance relative (4,77 secondes pour MemberwiseCopy imbriqué profond par rapport à 39,93 secondes pour une sérialisation). MemberwiseCopy imbriqué est presque aussi rapide que de copier une structure, et copier une structure est sacrément proche de la vitesse maximale théorique dont NET est capable, ce qui est probablement assez proche de la vitesse de la même chose en C ou C++ (mais exécuter des tests équivalents pour vérifier cette affirmation).

    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

Pour comprendre comment faire une copie en profondeur en utilisant MemberwiseCopy, voici le projet de démonstration:

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

Ensuite, appelez la démo depuis main:

    void MyMain(string[] args)
    {
        {
            Console.Write("Demo of shallow and deep copy, using classes and MemberwiseCopy:\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();
    }

Encore une fois, notez que if vous utilisez Nested MemberwiseClone pour une copie complète, vous devez implémenter manuellement un ShallowCopy pour chaque niveau imbriqué de la classe, et un DeepCopy qui appelle tous ces éléments. Méthodes ShallowCopy pour créer un clone complet. C'est simple: seulement quelques lignes au total, voir le code de démonstration ci-dessus.

Notez que lorsqu'il s'agit de cloner un objet, il existe une grande différence entre une "struct" et une "classe":

  • Si vous avez un "struct", c'est un type de valeur, vous pouvez donc le copier, et le contenu sera cloné.
  • Si vous avez une "classe", c'est un type de référence, donc si vous la copiez, vous ne faites que copier le pointeur sur celle-ci. Pour créer un vrai clone, vous devez être plus créatif et utiliser une méthode qui crée une autre copie de l'objet d'origine en mémoire.
  • Cloner des objets de manière incorrecte peut conduire à des bogues très difficiles à cerner. Dans le code de production, j'ai tendance à mettre en œuvre une somme de contrôle pour vérifier que l'objet a été correctement cloné et qu'il n'a pas été corrompu par une autre référence. Cette somme de contrôle peut être désactivée en mode Release.
  • Je trouve cette méthode assez utile: souvent, vous ne voulez que cloner des parties de l'objet, pas la totalité. Il est également essentiel pour tout cas d'utilisation où vous modifiez des objets, puis insérez les copies modifiées dans une file d'attente.

Mise à jour

Il est probablement possible d'utiliser la réflexion pour parcourir de manière récursive le graphe d'objets afin d'effectuer une copie en profondeur. WCF utilise cette technique pour sérialiser un objet, y compris tous ses enfants. L'astuce consiste à annoter tous les objets enfants avec un attribut qui le rend détectable. Vous pourriez toutefois perdre certains avantages en termes de performances.

Mise à jour

Citation sur le test de vitesse indépendant (voir commentaires ci-dessous):

J'ai effectué mon propre test de vitesse à l'aide de la méthode d'extension sérialisée/désérialisée de Neil, de Nested MemberwiseClone de Contango, de la méthode d'extension basée sur la réflexion d'Alex Burtsev et d'AutoMapper, 1 million de fois chacun. La sérialisation-désérialisation a été la plus lente, prenant 15,7 secondes. Puis vint AutoMapper, prenant 10,1 secondes. La méthode basée sur la réflexion, qui prenait 2,4 secondes, était beaucoup plus rapide. De loin le plus rapide était Nested MemberwiseClone, prenant 0,1 seconde. Cela revient à la performance plutôt qu’à la difficulté d’ajouter du code à chaque classe pour le cloner. Si la performance n’est pas un problème, utilisez la méthode d’Alex Burtsev. - Simon Tewsi

51
Contango

Je crois que l'approche de BinaryFormatter est relativement lente (ce qui m'a surpris!). Vous pourrez peut-être utiliser ProtoBuf .NET pour certains objets s'ils répondent aux exigences de ProtoBuf. À partir de la page Mise en route de ProtoBuf ( http://code.google.com/p/protobuf-net/wiki/GettingStarted ):

Notes sur les types supportés:

Cours personnalisés qui:

  • Sont marqués comme contrat de données
  • Avoir un constructeur sans paramètre
  • Pour Silverlight: sont publics
  • Beaucoup de primitives communes, etc.
  • Tableaux de dimensions uniques : T []
  • Liste <T>/IList <T>
  • Dictionnaire <TKey, TValue>/IDictionary <TKey, TValue>
  • tout type qui implémente IEnumerable <T> et possède une méthode Add (T)

Le code suppose que les types seront mutables autour des membres élus. En conséquence, les structures personnalisées ne sont pas prises en charge, car elles doivent être immuables.

Si votre classe répond à ces exigences, vous pouvez essayer:

public static void deepCopy<T>(ref T object2Copy, ref T objectCopy)
{
    using (var stream = new MemoryStream())
    {
        Serializer.Serialize(stream, object2Copy);
        stream.Position = 0;
        objectCopy = Serializer.Deserialize<T>(stream);
    }
}

Ce qui est très rapide en effet ...

Modifier:

Voici le code de travail pour une modification de celui-ci (testé sur .NET 4.6). Il utilise System.Xml.Serialization et System.IO. Pas besoin de marquer les classes comme sérialisables.

public void DeepCopy<T>(ref T object2Copy, ref T objectCopy)
{
    using (var stream = new MemoryStream())
    {
        var serializer = new XS.XmlSerializer(typeof(T));

        serializer.Serialize(stream, object2Copy);
        stream.Position = 0;
        objectCopy = (T)serializer.Deserialize(stream);
    }
}
15
Kurt Richardson

Tu peux essayer ça

    public static object DeepCopy(object obj)
    {
        if (obj == null)
            return null;
        Type type = obj.GetType();

        if (type.IsValueType || type == typeof(string))
        {
            return obj;
        }
        else if (type.IsArray)
        {
            Type elementType = Type.GetType(
                 type.FullName.Replace("[]", string.Empty));
            var array = obj as Array;
            Array copied = Array.CreateInstance(elementType, array.Length);
            for (int i = 0; i < array.Length; i++)
            {
                copied.SetValue(DeepCopy(array.GetValue(i)), i);
            }
            return Convert.ChangeType(copied, obj.GetType());
        }
        else if (type.IsClass)
        {

            object toret = Activator.CreateInstance(obj.GetType());
            FieldInfo[] fields = type.GetFields(BindingFlags.Public |
                        BindingFlags.NonPublic | BindingFlags.Instance);
            foreach (FieldInfo field in fields)
            {
                object fieldValue = field.GetValue(obj);
                if (fieldValue == null)
                    continue;
                field.SetValue(toret, DeepCopy(fieldValue));
            }
            return toret;
        }
        else
            throw new ArgumentException("Unknown type");
    }

Merci à DetoX83 article sur le projet de code.

Peut-être n’avez-vous besoin que d’une copie superficielle, dans ce cas, utilisez Object.MemberWiseClone().

Il existe de bonnes recommandations dans la documentation de MemberWiseClone() pour les stratégies de copie en profondeur: -

http://msdn.Microsoft.com/en-us/library/system.object.memberwiseclone.aspx

4
David Thornley

Le meilleur moyen est:

    public interface IDeepClonable<T> where T : class
    {
        T DeepClone();
    }

    public class MyObj : IDeepClonable<MyObj>
    {
        public MyObj Clone()
        {
            var myObj = new MyObj();
            myObj._field1 = _field1;//value type
            myObj._field2 = _field2;//value type
            myObj._field3 = _field3;//value type

            if (_child != null)
            {
                myObj._child = _child.DeepClone(); //reference type .DeepClone() that does the same
            }

            int len = _array.Length;
            myObj._array = new MyObj[len]; // array / collection
            for (int i = 0; i < len; i++)
            {
                myObj._array[i] = _array[i];
            }

            return myObj;
        }

        private bool _field1;
        public bool Field1
        {
            get { return _field1; }
            set { _field1 = value; }
        }

        private int _field2;
        public int Property2
        {
            get { return _field2; }
            set { _field2 = value; }
        }

        private string _field3;
        public string Property3
        {
            get { return _field3; }
            set { _field3 = value; }
        }

        private MyObj _child;
        private MyObj Child
        {
            get { return _child; }
            set { _child = value; }
        }

        private MyObj[] _array = new MyObj[4];
    }
3
alex

La documentation MSDN semble indiquer que Clone doit effectuer une copie complète, mais cela n’est jamais explicitement indiqué:

L'interface ICloneable contient un membre, Clone, destiné à prendre en charge le clonage au-delà de celui fourni par MemberWiseClone… La méthode MemberwiseClone crée une copie superficielle…

Vous pouvez trouver mon message utile.

http://pragmaticcoding.com/index.php/cloning-objects-in-c/

0
Eugene
    public static object CopyObject(object input)
    {
        if (input != null)
        {
            object result = Activator.CreateInstance(input.GetType());
            foreach (FieldInfo field in input.GetType().GetFields(Consts.AppConsts.FullBindingList))
            {
                if (field.FieldType.GetInterface("IList", false) == null)
                {
                    field.SetValue(result, field.GetValue(input));
                }
                else
                {
                    IList listObject = (IList)field.GetValue(result);
                    if (listObject != null)
                    {
                        foreach (object item in ((IList)field.GetValue(input)))
                        {
                            listObject.Add(CopyObject(item));
                        }
                    }
                }
            }
            return result;
        }
        else
        {
            return null;
        }
    }

Cette méthode est quelques fois plus rapide que BinarySerialization ET elle ne nécessite pas l'attribut [Serializable].

0
Basil