Objetos de clonación profunda

Resuelto NakedBrunch asked hace 15 años • 59 respuestas

Quiero hacer algo como:

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

Y luego realice cambios en el nuevo objeto que no se reflejen en el objeto original.

No suelo necesitar esta funcionalidad, así que cuando ha sido necesaria he recurrido a crear un nuevo objeto y luego copiar cada propiedad individualmente, pero siempre me deja con la sensación de que hay una forma mejor o más elegante de manejar la situación.

¿Cómo puedo clonar o copiar en profundidad un objeto para que el objeto clonado pueda modificarse sin que ningún cambio se refleje en el objeto original?

NakedBrunch avatar Sep 17 '08 07:09 NakedBrunch
Aceptado

Mientras que un enfoque es implementar la ICloneableinterfaz (descrita aquí , así que no regurgitaré), aquí hay una buena copiadora de objetos de clonación profunda que encontré en The Code Project hace un tiempo y la incorporé a nuestro código. Como se mencionó en otra parte, requiere que sus objetos sean serializables.

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 via serialization.
    /// </summary>
    /// <typeparam name="T">The type of object being copied.</typeparam>
    /// <param name="source">The object instance to copy.</param>
    /// <returns>A deep copy of the object.</returns>
    public static T Clone<T>(T source)
    {
        if (!typeof(T).IsSerializable)
        {
            throw new ArgumentException("The type must be serializable.", nameof(source));
        }

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

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

La idea es que serialice su objeto y luego lo deserialice en un objeto nuevo. El beneficio es que no tienes que preocuparte por clonar todo cuando un objeto se vuelve demasiado complejo.

En caso de que prefiera utilizar los nuevos métodos de extensión de C# 3.0, cambie el método para que tenga la siguiente firma:

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

Ahora la llamada al método simplemente se convierte en objectBeingCloned.Clone();.

EDITAR (10 de enero de 2015) Pensé en volver a visitar esto, para mencionar que recientemente comencé a usar (Newtonsoft) Json para hacer esto, debería ser más liviano y evita la sobrecarga de etiquetas [Serializables]. ( NB @atconway ha señalado en los comentarios que los miembros privados no se clonan utilizando el método JSON)

/// <summary>
/// Perform a deep Copy of the object, using Json as a serialization 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 (ReferenceEquals(source, null)) return default;

    // 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);
}
johnc avatar Sep 17 '2008 00:09 johnc

Quería un clonador para objetos muy simples, en su mayoría primitivos y listas. Si su objeto es serializable JSON listo para usar, este método funcionará. Esto no requiere ninguna modificación o implementación de interfaces en la clase clonada, solo un serializador JSON como JSON.NET.

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

Además, puedes utilizar este método de extensión.

public static class SystemExtension
{
    public static T Clone<T>(this T source)
    {
        var serialized = JsonConvert.SerializeObject(source);
        return JsonConvert.DeserializeObject<T>(serialized);
    }
}
craastad avatar Apr 03 '2013 13:04 craastad

La razón para no utilizar ICloneable no es porque no tenga una interfaz genérica. La razón para no usarlo es porque es vago . No deja claro si está obteniendo una copia superficial o profunda; eso depende del implementador.

Sí, MemberwiseClonehace una copia superficial, pero lo contrario MemberwiseCloneno lo es Clone; sería, tal vez, DeepClonelo que no existe. Cuando utiliza un objeto a través de su interfaz ICloneable, no puede saber qué tipo de clonación realiza el objeto subyacente. (Y los comentarios XML no lo dejarán claro, porque obtendrá los comentarios de la interfaz en lugar de los del método Clonar del objeto).

Lo que suelo hacer es simplemente crear un Copymétodo que haga exactamente lo que quiero.

Ryan Lundy avatar Sep 17 '2008 01:09 Ryan Lundy

Después de leer mucho sobre muchas de las opciones vinculadas aquí y las posibles soluciones para este problema, creo que todas las opciones se resumen bastante bien en el enlace de Ian P (todas las demás opciones son variaciones de ellas) y la mejor solución la proporciona Enlace de Pedro77 en los comentarios de la pregunta.

Así que simplemente copiaré aquí las partes relevantes de esas 2 referencias. De esa manera podemos tener:

¡Lo mejor que puedes hacer para clonar objetos en C sostenido!

En primer lugar, esas son todas nuestras opciones:

  • Manualmente con ICloneable , que es superficial y no seguro para tipos
  • MemberwiseClone , que utiliza ICloneable
  • Reflexión mediante el uso de Activator.CreateInstance y MemberwiseClone recursivo
  • Serialización , como lo señala la respuesta preferida de Johnc
  • Lenguaje intermedio , que no tengo idea de cómo funciona.
  • Métodos de extensión , como este marco de clonación personalizado de Havard Straden
  • Árboles de expresión

El artículo Fast Deep Copy by Expression Trees también compara el rendimiento de la clonación mediante serialización, reflexión y árboles de expresión.

Por qué elijo ICloneable (es decir, manualmente)

Venkat Subramaniam (enlace redundante aquí) explica con mucho detalle por qué .

Todo su artículo gira en torno a un ejemplo que intenta ser aplicable a la mayoría de los casos, utilizando 3 objetos: Persona , Cerebro y Ciudad . Queremos clonar a una persona, que tendrá su propio cerebro pero la misma ciudad. Puede imaginarse todos los problemas que cualquiera de los otros métodos anteriores puede generar o leer el artículo.

Esta es mi versión ligeramente modificada de su conclusión:

Copiar un objeto especificando Newseguido del nombre de la clase a menudo conduce a un código que no es extensible. Usar clon, la aplicación del patrón prototipo, es una mejor manera de lograrlo. Sin embargo, usar clone tal como se proporciona en C# (y Java) también puede ser bastante problemático. Es mejor proporcionar un constructor de copia protegido (no público) e invocarlo desde el método de clonación. Esto nos brinda la capacidad de delegar la tarea de crear un objeto a una instancia de una clase misma, proporcionando así extensibilidad y también creando los objetos de forma segura utilizando el constructor de copia protegida.

Esperemos que esta implementación pueda aclarar las cosas:

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

Ahora considere tener una clase derivada de Persona.

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

Puede intentar ejecutar el siguiente código:

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

El resultado obtenido será:

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

Observe que, si mantenemos un recuento del número de objetos, el clon implementado aquí mantendrá un recuento correcto del número de objetos.

cregox avatar Sep 26 '2012 20:09 cregox

Prefiero un constructor de copias a un clon. La intención es más clara.

Nick avatar Sep 17 '2008 00:09 Nick