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

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

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

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

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

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

Ответы [ 41 ]

1595 голосов
/ 17 сентября 2008

Хотя стандартная практика заключается в реализации интерфейса ICloneable (описан здесь , так что я не буду извергаться), вот хороший глубокий клонировщик объектов, который я нашел на The Code Project некоторое время назад и включил его в наши материалы.

Как уже упоминалось, требуется, чтобы ваши объекты были сериализуемыми.

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

/// <summary>
/// Reference Article http://www.codeproject.com/KB/tips/SerializedObjectCloner.aspx
/// Provides a method for performing a deep copy of an object.
/// Binary Serialization is used to perform the copy.
/// </summary>
public static class ObjectCopier
{
    /// <summary>
    /// Perform a deep Copy of the object.
    /// </summary>
    /// <typeparam name="T">The type of object being copied.</typeparam>
    /// <param name="source">The object instance to copy.</param>
    /// <returns>The copied object.</returns>
    public static T Clone<T>(T source)
    {
        if (!typeof(T).IsSerializable)
        {
            throw new ArgumentException("The type must be serializable.", "source");
        }

        // Don't serialize a null object, simply return the default for that object
        if (Object.ReferenceEquals(source, null))
        {
            return default(T);
        }

        IFormatter formatter = new BinaryFormatter();
        Stream stream = new MemoryStream();
        using (stream)
        {
            formatter.Serialize(stream, source);
            stream.Seek(0, SeekOrigin.Begin);
            return (T)formatter.Deserialize(stream);
        }
    }
}

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

И с использованием методов расширения (также из исходного источника):

Если вы предпочитаете использовать новые методы расширения в C # 3.0, измените метод так, чтобы он имел следующую подпись:

public static T Clone<T>(this T source)
{
   //...
}

Теперь вызов метода просто становится objectBeingCloned.Clone();.

РЕДАКТИРОВАТЬ (10 января 2015 г.) Я подумал, что я еще раз вернусь к этому, чтобы упомянуть, что недавно начал использовать (Newtonsoft) Json для этого, он должен быть легче и избегает накладные расходы на теги [Serializable]. ( NB @atconway указал в комментариях, что частные члены не клонируются с использованием метода JSON)

/// <summary>
/// Perform a deep Copy of the object, using Json as a serialisation method. NOTE: Private members are not cloned using this method.
/// </summary>
/// <typeparam name="T">The type of object being copied.</typeparam>
/// <param name="source">The object instance to copy.</param>
/// <returns>The copied object.</returns>
public static T CloneJson<T>(this T source)
{            
    // Don't serialize a null object, simply return the default for that object
    if (Object.ReferenceEquals(source, null))
    {
        return default(T);
    }

    // initialize inner objects individually
    // for example in default constructor some list property initialized with some values,
    // but in 'source' these items are cleaned -
    // without ObjectCreationHandling.Replace default constructor values will be added to result
    var deserializeSettings = new JsonSerializerSettings {ObjectCreationHandling = ObjectCreationHandling.Replace};

    return JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(source), deserializeSettings);
}
242 голосов
/ 03 апреля 2013

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

public static T Clone<T>(T source)
{
    var serialized = JsonConvert.SerializeObject(source);
    return JsonConvert.DeserializeObject<T>(serialized);
}

Также вы можете использовать этот метод расширения

public static class SystemExtension
{
    public static T Clone<T>(this T source)
    {
        var serialized = JsonConvert.SerializeObject(source);
        return JsonConvert.DeserializeObject<T>(serialized);
    }
}
162 голосов
/ 17 сентября 2008

Причина, по которой не следует использовать ICloneable , заключается в , а не , поскольку он не имеет универсального интерфейса. Причина, по которой он не используется, заключается в том, что он расплывчатый . Не ясно, получаете ли вы мелкую или глубокую копию; это до исполнителя.

Да, MemberwiseClone делает мелкую копию, но противоположность MemberwiseClone не Clone; возможно, это будет DeepClone, которого не существует. Когда вы используете объект через его интерфейс ICloneable, вы не можете знать, какой тип клонирования выполняет базовый объект. (И комментарии XML не прояснят это, потому что вы получите комментарии интерфейса, а не комментарии к методу Clone объекта.)

Обычно я просто делаю Copy метод, который делает именно то, что я хочу.

102 голосов
/ 27 сентября 2012

После большого количества прочтения о многих связанных здесь опциях и возможных решениях этой проблемы, я полагаю, все опции суммированы довольно хорошо по ссылке Ian P ( все остальные варианты являются их вариантами), и наилучшее решение предоставляется ссылкой Pedro77 в комментариях к вопросу.

Так что я просто скопирую соответствующие части этих двух ссылок здесь. Таким образом, мы можем иметь:

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

Прежде всего, это все наши варианты:

Статья Fast Deep Copy по деревьям выражений также имеет сравнение производительности клонирования по деревьям сериализации, отражения и выражения.

Почему я выбираю ICloneable (т.е. вручную)

Г-н Венкат Субраманиам (избыточная ссылка здесь) подробно объясняет, почему .

Вся его статья обводится вокруг примера, который пытается быть применимым для большинства случаев, используя 3 объекта: Человек , Мозг и Город . Мы хотим клонировать человека, у которого будет свой мозг, но тот же город. Вы можете изобразить все проблемы, которые могут принести другие методы, описанные выше, или прочитать статью.

Это моя слегка измененная версия его заключения:

Копирование объекта путем указания New с последующим именем класса часто приводит к тому, что код не является расширяемым. Использование клона, применение шаблона прототипа, является лучшим способом для достижения этой цели. Однако использование клона, как это предусмотрено в C # (и Java), также может быть довольно проблематичным. Лучше предоставить защищенный (непубличный) конструктор копирования и вызвать его из метода clone. Это дает нам возможность делегировать задачу создания объекта экземпляру самого класса, обеспечивая таким образом расширяемость, а также безопасное создание объектов с помощью конструктора защищенной копии.

Надеюсь, эта реализация прояснит ситуацию:

public class Person : ICloneable
{
    private final Brain brain; // brain is final since I do not want 
                // any transplant on it once created!
    private int age;
    public Person(Brain aBrain, int theAge)
    {
        brain = aBrain; 
        age = theAge;
    }
    protected Person(Person another)
    {
        Brain refBrain = null;
        try
        {
            refBrain = (Brain) another.brain.clone();
            // You can set the brain in the constructor
        }
        catch(CloneNotSupportedException e) {}
        brain = refBrain;
        age = another.age;
    }
    public String toString()
    {
        return "This is person with " + brain;
        // Not meant to sound rude as it reads!
    }
    public Object clone()
    {
        return new Person(this);
    }
    …
}

Теперь рассмотрим класс, производный от Person.

public class SkilledPerson extends Person
{
    private String theSkills;
    public SkilledPerson(Brain aBrain, int theAge, String skills)
    {
        super(aBrain, theAge);
        theSkills = skills;
    }
    protected SkilledPerson(SkilledPerson another)
    {
        super(another);
        theSkills = another.theSkills;
    }

    public Object clone()
    {
        return new SkilledPerson(this);
    }
    public String toString()
    {
        return "SkilledPerson: " + super.toString();
    }
}

Вы можете попробовать запустить следующий код:

public class User
{
    public static void play(Person p)
    {
        Person another = (Person) p.clone();
        System.out.println(p);
        System.out.println(another);
    }
    public static void main(String[] args)
    {
        Person sam = new Person(new Brain(), 1);
        play(sam);
        SkilledPerson bob = new SkilledPerson(new SmarterBrain(), 1, "Writer");
        play(bob);
    }
}

Результат будет:

This is person with Brain@1fcc69
This is person with Brain@253498
SkilledPerson: This is person with SmarterBrain@1fef6f
SkilledPerson: This is person with SmarterBrain@209f4e

Заметьте, что если мы будем вести подсчет количества объектов, то клон, реализованный здесь, будет вести правильный подсчет количества объектов.

77 голосов
/ 17 сентября 2008

Я предпочитаю конструктор копирования клону. Намерение яснее.

38 голосов
/ 16 марта 2011

Простой метод расширения для копирования всех открытых свойств. Работает для любых объектов, и для не требуется, чтобы класс был [Serializable]. Может быть расширен для другого уровня доступа.

public static void CopyTo( this object S, object T )
{
    foreach( var pS in S.GetType().GetProperties() )
    {
        foreach( var pT in T.GetType().GetProperties() )
        {
            if( pT.Name != pS.Name ) continue;
            ( pT.GetSetMethod() ).Invoke( T, new object[] 
            { pS.GetGetMethod().Invoke( S, null ) } );
        }
    };
}
30 голосов
/ 02 декабря 2009

Что ж, у меня были проблемы с использованием ICloneable в Silverlight, но мне понравилась идея разделения, я могу отделить XML, поэтому я сделал это:

static public class SerializeHelper
{
    //Michael White, Holly Springs Consulting, 2009
    //michael@hollyspringsconsulting.com
    public static T DeserializeXML<T>(string xmlData) where T:new()
    {
        if (string.IsNullOrEmpty(xmlData))
            return default(T);

        TextReader tr = new StringReader(xmlData);
        T DocItms = new T();
        XmlSerializer xms = new XmlSerializer(DocItms.GetType());
        DocItms = (T)xms.Deserialize(tr);

        return DocItms == null ? default(T) : DocItms;
    }

    public static string SeralizeObjectToXML<T>(T xmlObject)
    {
        StringBuilder sbTR = new StringBuilder();
        XmlSerializer xmsTR = new XmlSerializer(xmlObject.GetType());
        XmlWriterSettings xwsTR = new XmlWriterSettings();

        XmlWriter xmwTR = XmlWriter.Create(sbTR, xwsTR);
        xmsTR.Serialize(xmwTR,xmlObject);

        return sbTR.ToString();
    }

    public static T CloneObject<T>(T objClone) where T:new()
    {
        string GetString = SerializeHelper.SeralizeObjectToXML<T>(objClone);
        return SerializeHelper.DeserializeXML<T>(GetString);
    }
}
27 голосов
/ 25 декабря 2013

Я только что создал CloneExtensions библиотека проект. Он выполняет быстрое глубокое клонирование с использованием простых операций присваивания, генерируемых компиляцией кода среды выполнения Expression Tree.

Как это использовать?

Вместо написания собственных методов Clone или Copy с тоном присвоений между полями и свойствами заставьте программу сделать это самостоятельно, используя Дерево выражений. GetClone<T>() метод, помеченный как метод расширения, позволяет просто вызвать его в вашем экземпляре:

var newInstance = source.GetClone();

Вы можете выбрать, что следует скопировать из source в newInstance, используя CloningFlags enum:

var newInstance 
    = source.GetClone(CloningFlags.Properties | CloningFlags.CollectionItems);

Что можно клонировать?

  • Примитив (int, uint, byte, double, char и т. Д.), Известный как неизменяемый типы (DateTime, TimeSpan, String) и делегаты (включая Action, Func и т. Д.)
  • Nullable
  • T [] массивы
  • Пользовательские классы и структуры, включая общие классы и структуры.

Следующие члены класса / структуры внутренне клонируются:

  • Значения открытых, не доступных для чтения полей
  • Значения общедоступных свойств с методами доступа get и set
  • Элементы коллекции для типов, реализующих ICollection

Как быстро?

Решение быстрее, чем рефлексия, потому что информация об участниках должна быть собрана только один раз, прежде чем GetClone<T> впервые будет использован для данного типа T.

Это также быстрее, чем решение на основе сериализации, когда вы клонируете более пары экземпляров одного типа T.

и более ...

Подробнее о сгенерированных выражениях читайте в документации .

Пример списка отладочных выражений для List<int>:

.Lambda #Lambda1<System.Func`4[System.Collections.Generic.List`1[System.Int32],CloneExtensions.CloningFlags,System.Collections.Generic.IDictionary`2[System.Type,System.Func`2[System.Object,System.Object]],System.Collections.Generic.List`1[System.Int32]]>(
    System.Collections.Generic.List`1[System.Int32] $source,
    CloneExtensions.CloningFlags $flags,
    System.Collections.Generic.IDictionary`2[System.Type,System.Func`2[System.Object,System.Object]] $initializers) {
    .Block(System.Collections.Generic.List`1[System.Int32] $target) {
        .If ($source == null) {
            .Return #Label1 { null }
        } .Else {
            .Default(System.Void)
        };
        .If (
            .Call $initializers.ContainsKey(.Constant<System.Type>(System.Collections.Generic.List`1[System.Int32]))
        ) {
            $target = (System.Collections.Generic.List`1[System.Int32]).Call ($initializers.Item[.Constant<System.Type>(System.Collections.Generic.List`1[System.Int32])]
            ).Invoke((System.Object)$source)
        } .Else {
            $target = .New System.Collections.Generic.List`1[System.Int32]()
        };
        .If (
            ((System.Byte)$flags & (System.Byte).Constant<CloneExtensions.CloningFlags>(Fields)) == (System.Byte).Constant<CloneExtensions.CloningFlags>(Fields)
        ) {
            .Default(System.Void)
        } .Else {
            .Default(System.Void)
        };
        .If (
            ((System.Byte)$flags & (System.Byte).Constant<CloneExtensions.CloningFlags>(Properties)) == (System.Byte).Constant<CloneExtensions.CloningFlags>(Properties)
        ) {
            .Block() {
                $target.Capacity = .Call CloneExtensions.CloneFactory.GetClone(
                    $source.Capacity,
                    $flags,
                    $initializers)
            }
        } .Else {
            .Default(System.Void)
        };
        .If (
            ((System.Byte)$flags & (System.Byte).Constant<CloneExtensions.CloningFlags>(CollectionItems)) == (System.Byte).Constant<CloneExtensions.CloningFlags>(CollectionItems)
        ) {
            .Block(
                System.Collections.Generic.IEnumerator`1[System.Int32] $var1,
                System.Collections.Generic.ICollection`1[System.Int32] $var2) {
                $var1 = (System.Collections.Generic.IEnumerator`1[System.Int32]).Call $source.GetEnumerator();
                $var2 = (System.Collections.Generic.ICollection`1[System.Int32])$target;
                .Loop  {
                    .If (.Call $var1.MoveNext() != False) {
                        .Call $var2.Add(.Call CloneExtensions.CloneFactory.GetClone(
                                $var1.Current,
                                $flags,


                         $initializers))
                } .Else {
                    .Break #Label2 { }
                }
            }
            .LabelTarget #Label2:
        }
    } .Else {
        .Default(System.Void)
    };
    .Label
        $target
    .LabelTarget #Label1:
}

}

что имеет такое же значение, как следующий код c #:

(source, flags, initializers) =>
{
    if(source == null)
        return null;

    if(initializers.ContainsKey(typeof(List<int>))
        target = (List<int>)initializers[typeof(List<int>)].Invoke((object)source);
    else
        target = new List<int>();

    if((flags & CloningFlags.Properties) == CloningFlags.Properties)
    {
        target.Capacity = target.Capacity.GetClone(flags, initializers);
    }

    if((flags & CloningFlags.CollectionItems) == CloningFlags.CollectionItems)
    {
        var targetCollection = (ICollection<int>)target;
        foreach(var item in (ICollection<int>)source)
        {
            targetCollection.Add(item.Clone(flags, initializers));
        }
    }

    return target;
}

Разве это не похоже на то, как вы бы написали свой собственный Clone метод для List<int>?

26 голосов
/ 15 октября 2012

Если вы уже используете стороннее приложение, например ValueInjecter или Automapper , вы можете сделать что-то вроде этого:

MyObject oldObj; // The existing object to clone

MyObject newObj = new MyObject();
newObj.InjectFrom(oldObj); // Using ValueInjecter syntax

Используя этот метод, вам не нужно реализовывать ISerializable или ICloneable на ваших объектах. Это характерно для паттерна MVC / MVVM, поэтому были созданы простые инструменты, подобные этому.

см. значение для глубокого клонирования в CodePlex .

20 голосов
/ 17 сентября 2008

Короткий ответ: вы наследуете от интерфейса ICloneable, а затем реализуете функцию .clone. Клон должен сделать для каждого члена копию и выполнить глубокое копирование любого члена, которому это требуется, а затем вернуть полученный объект. Это рекурсивная операция (она требует, чтобы все члены класса, который вы хотите клонировать, были либо типами значений, либо реализовали ICloneable, и чтобы их члены были либо типами значений, либо реализовали ICloneable и т. Д.).

Более подробное объяснение клонирования с использованием ICloneable можно найти в этой статье .

Ответ long «зависит». Как уже упоминалось, ICloneable не поддерживается обобщениями, требует особых соображений для циклических ссылок на классы, и на самом деле некоторые воспринимаются как «ошибка» в .NET Framework. Метод сериализации зависит от того, ваши объекты могут быть сериализуемыми, а они могут отсутствовать, и вы не можете контролировать их. В сообществе все еще много споров о том, что является «лучшей» практикой. На самом деле, ни одно из решений не является универсальным, подходящим для всех лучших практик, для всех ситуаций, в которых ICloneable изначально интерпретировался.

См. Эту статью «Уголок разработчика» , где описаны еще несколько вариантов (благодарность Иану).

...