Глубокое клонирование объектов - PullRequest
2035 голосов
/ 17 сентября 2008

Я хочу сделать что-то вроде:

MyObject myObj = GetMyObj(); // Create and fill a new object
MyObject newObj = myObj.Clone();

И затем внесите изменения в новый объект, которые не отражены в исходном объекте.

Мне не часто нужны эти функции, поэтому, когда это было необходимо, я прибегал к созданию нового объекта, а затем копировал каждое свойство по отдельности, но это всегда оставляло у меня ощущение, что есть лучший или более элегантный способ справиться с ситуацией.

Как я могу клонировать или выполнить глубокое копирование объекта, чтобы клонированный объект мог быть изменен без каких-либо изменений, отраженных в исходном объекте?

Ответы [ 41 ]

19 голосов
/ 04 августа 2016

Лучше всего реализовать метод расширения , как

public static T DeepClone<T>(this T originalObject)
{ /* the cloning code */ }

и затем используйте его в любом месте решения

var copy = anyObject.DeepClone();

Мы можем иметь следующие три реализации:

  1. По сериализации (самый короткий код)
  2. Отражением - в 5 раз быстрее
  3. По деревьям выражений - 20x быстрее

Все связанные методы хорошо работают и были тщательно протестированы.

16 голосов
/ 17 сентября 2008
  1. В основном вам нужно реализовать интерфейс ICloneable, а затем реализовать копирование структуры объекта.
  2. Если это полная копия всех участников, вам нужно убедиться (не относясь к выбранному вами решению), что все дети также являются клонируемыми.
  3. Иногда вам нужно знать о некоторых ограничениях во время этого процесса, например, если вы копируете объекты ORM, большинство фреймворков допускает только один объект, присоединенный к сеансу, и вы НЕ ДОЛЖНЫ создавать клоны этого объекта, или если это возможно, вы нужно позаботиться о присоединении этих объектов к сеансу.

Приветствие.

15 голосов
/ 16 февраля 2015

Если вы хотите истинное клонирование неизвестных типов, вы можете посмотреть на fastclone .

Это клонирование на основе выражений, работающее примерно в 10 раз быстрее, чем двоичная сериализация, и поддерживающее полную целостность графов объектов.

Это означает: если вы несколько раз ссылаетесь на один и тот же объект в своей иерархии, клон также будет иметь ссылку на один экземпляр.

Нет необходимости в интерфейсах, атрибутах или любых других модификациях клонируемых объектов.

11 голосов
/ 28 мая 2016

Сохраняйте простоту и используйте AutoMapper , как уже упоминалось, это простая маленькая библиотека для отображения одного объекта на другой ... Чтобы скопировать объект на другой с тем же типом, все, что вам нужно, это три строки кода:

MyType source = new MyType();
Mapper.CreateMap<MyType, MyType>();
MyType target = Mapper.Map<MyType, MyType>(source);

Целевой объект теперь является копией исходного объекта. Не достаточно просто? Создайте метод расширения, который будет использоваться везде в вашем решении:

public static T Copy<T>(this T source)
{
    T copy = default(T);
    Mapper.CreateMap<T, T>();
    copy = Mapper.Map<T, T>(source);
    return copy;
}

При использовании метода расширения три строки становятся одной строкой:

MyType copy = source.Copy();
10 голосов
/ 30 сентября 2009

Я придумал это, чтобы преодолеть недостаток .NET , который требовал глубокого копирования списка вручную .

.

Я использую это:

static public IEnumerable<SpotPlacement> CloneList(List<SpotPlacement> spotPlacements)
{
    foreach (SpotPlacement sp in spotPlacements)
    {
        yield return (SpotPlacement)sp.Clone();
    }
}

А в другом месте:

public object Clone()
{
    OrderItem newOrderItem = new OrderItem();
    ...
    newOrderItem._exactPlacements.AddRange(SpotPlacement.CloneList(_exactPlacements));
    ...
    return newOrderItem;
}

Я пытался придумать oneliner, который делает это, но это невозможно, так как yield не работает внутри блоков анонимного метода.

Еще лучше, используйте общий список клонер:

class Utility<T> where T : ICloneable
{
    static public IEnumerable<T> CloneList(List<T> tl)
    {
        foreach (T t in tl)
        {
            yield return (T)t.Clone();
        }
    }
}
8 голосов
/ 04 июля 2015

Q. Почему я выбрал этот ответ?

  • Выберите этот ответ, если вы хотите самую быструю скорость, на которую способен .NET.
  • Игнорируйте этот ответ, если вы хотите действительно очень простой метод клонирования.

Другими словами, используйте другой ответ, если только у вас нет узкого места в производительности, которое необходимо исправить, и вы можете доказать это с помощью профилировщика .

в 10 раз быстрее, чем другие методы

Следующий метод выполнения глубокого клона:

  • в 10 раз быстрее, чем все, что связано с сериализацией / десериализацией;
  • Довольно чертовски близко к теоретической максимальной скорости, на которую способен .NET.

и метод ...

Для максимальной скорости вы можете использовать Nested MemberwiseClone для создания глубокой копии . Это почти такая же скорость, как при копировании структуры значений, и намного быстрее, чем (а) отражение или (б) сериализация (как описано в других ответах на этой странице).

Обратите внимание, что , если вы используете Nested MemberwiseClone для глубокой копии , вам необходимо вручную реализовать ShallowCopy для каждого вложенного уровня в классе и DeepCopy, который вызывает все указанные ShallowCopy методы для создания полного клона. Это просто: всего несколько строк, см. Демонстрационный код ниже.

Вот вывод кода, показывающего относительную разницу в производительности для 100 000 клонов:

  • 1,08 секунды для вложенного MemberwiseClone на вложенных структурах
  • 4,77 секунды для вложенного MemberwiseClone во вложенных классах
  • 39,93 секунды для сериализации / десериализации

Использование Nested MemberwiseClone в классе почти так же быстро, как копирование структуры, и копирование структуры чертовски близко к теоретической максимальной скорости, на которую способна .NET.

Demo 1 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 2 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 3 of deep copy, using class and serialize/deserialize:
  Elapsed time: 00:00:39.9339425,30000000

Чтобы понять, как сделать глубокое копирование с помощью MemberwiseCopy, вот демонстрационный проект, который использовался для генерации вышеуказанного времени:

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

Затем вызовите демо с основного:

void MyMain(string[] args)
{
    {
        Console.Write("Demo 1 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\n", sw.Elapsed, total);
    }
    {               
        Console.Write("Demo 2 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\n", sw.Elapsed, total);
    }
    {
        Console.Write("Demo 3 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();
}

Опять же, обратите внимание, что , если вы используете Nested MemberwiseClone для глубокой копии , вы должны вручную реализовать ShallowCopy для каждого вложенного уровня в классе и DeepCopy, который вызывает все сказал ShallowCopy методы для создания полного клона. Это просто: всего несколько строк, см. Демонстрационный код выше.

Типы значений и типы ссылок

Обратите внимание, что когда дело доходит до клонирования объекта, существует большая разница между " struct " и " class ":

  • Если у вас есть « struct », это тип значения , так что вы можете просто скопировать его, и содержимое будет клонировано (но это будет только мелкий клон, если только Вы используете техники в этом посте).
  • Если у вас есть « class », это ссылочный тип , поэтому, если вы копируете его, все, что вы делаете, это копируете указатель на него. Чтобы создать настоящий клон, вы должны быть более креативными и использовать различия между типами значений и ссылочными типами , что создает другую копию исходного объекта в памяти.

См. различия между типами значений и ссылочными типами .

Контрольные суммы для помощи в отладке

  • Неправильное клонирование объектов может привести к очень сложным ошибкам. В рабочем коде я склонен реализовывать контрольную сумму, чтобы дважды проверить, что объект был клонирован правильно и не был поврежден из-за другой ссылки на него. Эту контрольную сумму можно отключить в режиме разблокировки.
  • Я считаю этот метод весьма полезным: часто вам нужно только клонировать части объекта, а не все.

Действительно полезно для отделения множества потоков от многих других потоков

Одним из отличных вариантов использования этого кода является подача клонов вложенного класса или структуры в очередь для реализации шаблона производитель / потребитель.

  • У нас может быть один (или несколько) потоков, которые изменяют принадлежащий им класс, а затем помещают полную копию этого класса в ConcurrentQueue.
  • Затем у нас есть один (или более) поток, извлекающий копии этих классов и работающий с ними.

На практике это работает очень хорошо и позволяет нам отделить множество потоков (производителей) от одного или нескольких потоков (потребителей).

И этот метод тоже невероятно быстр: если мы используем вложенные структуры, это в 35 раз быстрее, чем сериализация / десериализация вложенных классов, и позволяет нам использовать все потоки, доступные на машине.

Обновление

Очевидно, что ExpressMapper работает так же быстро, если не быстрее, чем ручное кодирование, как описано выше. Возможно, мне придется посмотреть, как они сравниваются с профилировщиком.

7 голосов
/ 19 октября 2010

Я видел, как это реализовано через отражение. По сути, существовал метод, который будет перебирать членов объекта и соответствующим образом копировать их в новый объект. Когда он достиг ссылочных типов или коллекций, я думаю, что он сделал рекурсивный вызов сам по себе. Отражение дорого, но сработало довольно хорошо.

7 голосов
/ 06 сентября 2011

Вот реализация глубокой копии:

public static object CloneObject(object opSource)
{
    //grab the type and create a new instance of that type
    Type opSourceType = opSource.GetType();
    object opTarget = CreateInstanceOfType(opSourceType);

    //grab the properties
    PropertyInfo[] opPropertyInfo = opSourceType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);

    //iterate over the properties and if it has a 'set' method assign it from the source TO the target
    foreach (PropertyInfo item in opPropertyInfo)
    {
        if (item.CanWrite)
        {
            //value types can simply be 'set'
            if (item.PropertyType.IsValueType || item.PropertyType.IsEnum || item.PropertyType.Equals(typeof(System.String)))
            {
                item.SetValue(opTarget, item.GetValue(opSource, null), null);
            }
            //object/complex types need to recursively call this method until the end of the tree is reached
            else
            {
                object opPropertyValue = item.GetValue(opSource, null);
                if (opPropertyValue == null)
                {
                    item.SetValue(opTarget, null, null);
                }
                else
                {
                    item.SetValue(opTarget, CloneObject(opPropertyValue), null);
                }
            }
        }
    }
    //return the new item
    return opTarget;
}
7 голосов
/ 17 сентября 2008

В общем, вы реализуете интерфейс ICloneable и внедряете Clone самостоятельно. Объекты C # имеют встроенный метод MemberwiseClone, который выполняет поверхностное копирование, которое может помочь вам для всех примитивов.

Для глубокой копии нет способа узнать, как автоматически это сделать.

7 голосов
/ 25 января 2016

Поскольку я не смог найти клонер, отвечающий всем моим требованиям в разных проектах, я создал глубокий клонер, который можно настраивать и адаптировать к различным структурам кода, вместо того, чтобы адаптировать свой код для удовлетворения требований клонеров. Это достигается путем добавления аннотаций к коду, который должен быть клонирован, или вы просто оставляете код так, как он имеет поведение по умолчанию. Он использует рефлексы, кэши типов и основан на fastflect . Процесс клонирования очень быстрый для огромного количества данных и высокой иерархии объектов (по сравнению с другими алгоритмами, основанными на отражении / сериализации).

https://github.com/kalisohn/CloneBehave

Также доступно в виде пакета Nuget: https://www.nuget.org/packages/Clone.Behave/1.0.0

Например: следующий код будет DeepClone Address, но будет выполнять только поверхностную копию поля _currentJob.

public class Person 
{
  [DeepClone(DeepCloneBehavior.Shallow)]
  private Job _currentJob;      

  public string Name { get; set; }

  public Job CurrentJob 
  { 
    get{ return _currentJob; }
    set{ _currentJob = value; }
  }

  public Person Manager { get; set; }
}

public class Address 
{      
  public Person PersonLivingHere { get; set; }
}

Address adr = new Address();
adr.PersonLivingHere = new Person("John");
adr.PersonLivingHere.BestFriend = new Person("James");
adr.PersonLivingHere.CurrentJob = new Job("Programmer");

Address adrClone = adr.Clone();

//RESULT
adr.PersonLivingHere == adrClone.PersonLivingHere //false
adr.PersonLivingHere.Manager == adrClone.PersonLivingHere.Manager //false
adr.PersonLivingHere.CurrentJob == adrClone.PersonLivingHere.CurrentJob //true
adr.PersonLivingHere.CurrentJob.AnyProperty == adrClone.PersonLivingHere.CurrentJob.AnyProperty //true
...