Pregunta Objetos de clonación profunda


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 reflejan en el objeto original.

A menudo no necesito esta funcionalidad, así que cuando ha sido necesario, he recurrido a crear un nuevo objeto y luego a copiar cada propiedad individualmente, pero siempre me da la sensación de que hay una forma mejor o más elegante de manejarlo. la situación.

¿Cómo puedo clonar o copiar profundamente un objeto para que el objeto clonado se pueda modificar sin que se reflejen cambios en el objeto original?


1827
2017-09-17 00:06


origen


Respuestas:


Mientras que la práctica estándar es implementar el ICloneable interfaz (descrita aquí, así que no voy a regurgitar), aquí hay una bonita copiadora de objetos de clonación profunda que encontré en El Proyecto de Código hace un tiempo y lo incorporó en nuestras cosas.

Como se mencionó en otra parte, sí 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.
    /// </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);
        }
    }
}

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

Y con el uso de métodos de extensión (también de la fuente originalmente referenciada):

En caso de que prefiera usar el nuevo métodos de extensión de C # 3.0, cambie el método para tener la siguiente firma:

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

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

EDITAR (10 de enero de 2015) Pensé en volver a visitar esto, mencionar que recientemente comencé a usar (Newtonsoft) Json para hacer esto, debiera ser más ligero, y evita la sobrecarga de las etiquetas [Serializable]. (nótese bien @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 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);
}

1466
2018-04-03 13:31



Quería un clonador para objetos muy simples de primitivos y listas en su mayoría. Si su objeto está fuera de la caja serializable JSON entonces este método hará el truco. Esto no requiere 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);
}

181
2017-09-17 01:12



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

Sí, MemberwiseClone hace una copia superficial, pero lo contrario de MemberwiseClone no es Clone; sería, tal vez, DeepCloneque 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 dejarán en claro, porque obtendrá los comentarios de la interfaz en lugar de los del método Clone del objeto).

Lo que suelo hacer es simplemente hacer un Copy método que hace exactamente lo que quiero.


146
2017-09-26 20:18



Después de leer mucho sobre muchas de las opciones relacionadas aquí, y las posibles soluciones para este problema, creo todas las opciones se resumen bastante bien en Ian Penlace (todas las otras opciones son variaciones de esas) y la mejor solución es provista por Pedro77enlace en los comentarios de la pregunta.

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

¡Lo mejor que se puede hacer por clonar objetos en c sharp!

En primer lugar, esas son todas nuestras opciones:

los artículo Fast Deep Copy de Expression Trees   también tiene una comparación de rendimiento de clonación por Serialización, Reflexión y Árboles de Expresión.

Por qué elijo ICloneable (es decir, de forma manual)

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

Todo su artículo gira alrededor de un ejemplo que intenta ser aplicable para 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 visualizar todos los problemas que cualquiera de los otros métodos anteriores puede traer o leer el artículo.

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

Copiando un objeto especificando New seguido del nombre de la clase a menudo conduce a un código que no es extensible. Usar la clonación, la aplicación del patrón prototipo, es una mejor manera de lograr esto. Sin embargo, usar clon como se proporciona en C # (y Java) puede ser bastante problemático también. Es mejor proporcionar un constructor de copia protegido (no público) e invocarlo desde el método de clonación. Esto nos da la capacidad de delegar la tarea de crear un objeto en una instancia de una clase en sí misma, proporcionando así extensibilidad y también, creando de forma segura los objetos 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);
    }
}

La producción producida 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 contamos el número de objetos, el clon, tal como se implementó aquí, mantendrá un recuento correcto de la cantidad de objetos.


83
2017-09-17 00:13



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


69
2018-03-16 11:38



Método de extensión simple para copiar todas las propiedades públicas. Funciona para cualquier objeto y no requiere clase para ser [Serializable]. Se puede extender para otro nivel de acceso.

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

36
2017-12-02 17:39



Bueno, estaba teniendo problemas para usar ICloneable en Silverlight, pero me gustó la idea de la separación, puedo seralizar XML, así que hice esto:

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

28
2017-10-15 17:55



Si ya está usando una aplicación de terceros como ValueInjecter o Automapper, puedes hacer algo como esto:

MyObject oldObj; // The existing object to clone

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

Usando este método, no es necesario implementar ISerializable o ICloneable en sus objetos. Esto es común con el patrón MVC / MVVM, por lo que se han creado herramientas simples como esta.

ver la solución de clonación profunda Valueinjecter en CodePlex.


26
2017-12-24 22:56



Acabo de crear CloneExtensions biblioteca proyecto. Realiza clones rápidos y profundos usando operaciones de asignación simples generadas por la compilación de código de tiempo de ejecución de Expression Tree.

¿Cómo usarlo?

En lugar de escribir tu propio Clone o Copy los métodos con un tono de asignaciones entre campos y propiedades hacen que el programa lo haga por usted mismo, utilizando Expression Tree. GetClone<T>() método marcado como método de extensión le permite simplemente llamarlo en su instancia:

var newInstance = source.GetClone();

Puedes elegir de qué se debe copiar source a newInstance utilizando CloningFlags enum:

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

¿Qué se puede clonar?

  • Primitivo (int, uint, byte, double, char, etc.), conocido inmutable tipos (DateTime, TimeSpan, String) y delegados (incluidos Acción, Func, etc.)
  • Nullable
  • T [] arrays
  • Clases y estructuras personalizadas, incluidas clases y estructuras genéricas.

Los siguientes miembros de clase / estructura se clonan internamente:

  • Valores de campos públicos, no de solo lectura
  • Valores de las propiedades públicas con accesos get y set
  • Artículos de colección para los tipos que implementan ICollection

¿Qué tan rápido es?

La solución es más rápida que la reflexión, porque la información de los miembros debe reunirse solo una vez, antes GetClone<T> se usa por primera vez para un tipo dado T.

También es más rápido que la solución basada en la serialización cuando clonas más de un par de instancias del mismo tipo T.

y más...

Obtenga más información sobre las expresiones generadas en documentación.

Ejemplo de lista de depuración de expresiones para 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:
}

}

lo que tiene el mismo significado, como seguir el código 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;
}

¿No es muy parecido a cómo escribirías tu propio Clone método para List<int>?


25
2017-09-17 00:14



La respuesta corta es heredar de la interfaz ICloneable y luego implementar la función .clone. El clon debe hacer una copia de miembro y realizar una copia profunda en cualquier miembro que lo requiera, luego devolver el objeto resultante. Esta es una operación recursiva (requiere que todos los miembros de la clase que desee clonar sean de tipo valor o implementen ICloneable y que sus miembros sean de tipo valor o implementen ICloneable, y así sucesivamente).

Para obtener una explicación más detallada sobre la clonación con ICloneable, echa un vistazo a Este artículo.

los largo la respuesta es "depende". Como lo mencionaron otros, ICloneable no es compatible con los genéricos, requiere consideraciones especiales para las referencias de clases circulares, y en realidad es visto por algunos como un "Error" en .NET Framework. El método de serialización depende de que sus objetos sean serializables, lo que puede no ser y de lo que puede no tener control. Todavía hay mucho debate en la comunidad sobre cuál es la "mejor" práctica. En realidad, ninguna de las soluciones es la mejor práctica para todas las situaciones como ICLoneable fue interpretada originalmente.

Mira esto Artículo de Developer's Corner para algunas opciones más (crédito a Ian).


20
2018-02-16 11:30