Necesito implementar constructores de copia profunda en C # con herencia. ¿Qué patrones hay para elegir?

Deseo implementar una copia en profundidad de la jerarquía de mis clases en C #

public Class ParentObj : ICloneable { protected int myA; public virtual Object Clone () { ParentObj newObj = new ParentObj(); newObj.myA = theObj.MyA; return newObj; } } public Class ChildObj : ParentObj { protected int myB; public override Object Clone ( ) { Parent newObj = this.base.Clone(); newObj.myB = theObj.MyB; return newObj; } } 

Esto no funcionará como cuando Cloning the Child solo un padre es nuevo. En mi código algunas clases tienen jerarquías grandes.

¿Cuál es la forma recomendada de hacer esto? ¿Clonar todo en cada nivel sin llamar a la clase base parece mal? Debe haber algunas soluciones claras para este problema, ¿cuáles son?

¿Puedo agradecer a todos por sus respuestas? Fue realmente interesante ver algunos de los enfoques. Creo que sería bueno que alguien diera un ejemplo de una respuesta de reflexión para completarla. +1 en espera!

El enfoque típico es usar el patrón “copiar constructor” a la C ++:

  class Base : ICloneable { int x; protected Base(Base other) { x = other.x; } public virtual object Clone() { return new Base(this); } } class Derived : Base { int y; protected Derived(Derived other) : Base(other) { y = other.y; } public override object Clone() { return new Derived(this); } } 

El otro enfoque es utilizar Object.MemberwiseClone en la implementación de la Clone ; esto garantizará que el resultado sea siempre del tipo correcto y permitirá que se amplíen las anulaciones:

  class Base : ICloneable { List xs; public virtual object Clone() { Base result = this.MemberwiseClone(); // xs points to same List object here, but we want // a new List object with copy of data result.xs = new List(xs); return result; } } class Derived : Base { List ys; public override object Clone() { // Cast is legal, because MemberwiseClone() will use the // actual type of the object to instantiate the copy. Derived result = (Derived)base.Clone(); // ys points to same List object here, but we want // a new List object with copy of data result.ys = new List(ys); return result; } } 

Ambos enfoques requieren que todas las clases en la jerarquía sigan el patrón. Cuál usar es una cuestión de preferencia.

Si solo tiene una clase aleatoria que implementa ICloneable sin garantías de implementación (aparte de seguir la semántica documentada de ICloneable ), no hay forma de extenderlo.

Prueba el truco de serialización:

 public object Clone(object toClone) { BinaryFormatter bf = new BinaryFormatter(); MemoryStream ms= new MemoryStream(); bf.Serialize(ms, toClone); ms.Flush(); ms.Position = 0; return bf.Deserialize(ms); } 

ADVERTENCIA:

Este código debe ser usado con mucha precaución. Úselo bajo su propio riesgo. Este ejemplo se proporciona tal como está y sin garantía de ningún tipo.


Hay otra forma de realizar un clon profundo en un gráfico de objetos. Es importante tener en cuenta lo siguiente al considerar el uso de esta muestra:

Contras:

  1. Cualquier referencia a clases externas también se clonará a menos que esas referencias se proporcionen al método Clonar (objeto, …).
  2. No se ejecutarán constructores en los objetos clonados, se reproducen EXACTAMENTE como están.
  3. No se ejecutarán constructores ISerializables o de serialización.
  4. No hay manera de alterar el comportamiento de este método en un tipo específico.
  5. Se clonará todo, Stream, AppDomain, Form, lo que sea, y esos romperán tu aplicación de maneras horribles.
  6. Podría romperse, mientras que el uso del método de serialización es mucho más probable que continúe funcionando.
  7. La implementación a continuación utiliza la recursión y puede causar un desbordamiento de stack fácilmente si su gráfico de objetos es demasiado profundo.

Entonces, ¿por qué querrías usarlo?

Pros:

  1. Realiza una copia completa completa de todos los datos de instancia sin necesidad de encoding en el objeto.
  2. Conserva todas las referencias gráficas de objeto (incluso circular) en el objeto reconstituido.
  3. Se ejecuta más de 20 veces más que el formateador binario con menos consumo de memoria.
  4. No requiere nada, ni atributos, interfaces implementadas, propiedades públicas, nada.

Código de uso:

Simplemente llámalo con un objeto:

 Class1 copy = Clone(myClass1); 

O digamos que tiene un objeto secundario y está suscrito a sus eventos … Ahora desea clonar ese objeto secundario. Al proporcionar una lista de objetos para no clonar, puede preservar una parte del gráfico de objetos:

 Class1 copy = Clone(myClass1, this); 

Implementación:

Ahora vamos a sacar las cosas fáciles del camino primero … Aquí está el punto de entrada:

 public static T Clone(T input, params object[] stableReferences) { Dictionary graph = new Dictionary(new ReferenceComparer()); foreach (object o in stableReferences) graph.Add(o, o); return InternalClone(input, graph); } 

Ahora que es bastante simple, simplemente construye un mapa de diccionario para los objetos durante la clonación y lo llena con cualquier objeto que no deba clonarse. Notará que el comparador proporcionado al diccionario es un ReferenceComparer, echemos un vistazo a lo que hace:

 class ReferenceComparer : IEqualityComparer { bool IEqualityComparer.Equals(object x, object y) { return Object.ReferenceEquals(x, y); } int IEqualityComparer.GetHashCode(object obj) { return RuntimeHelpers.GetHashCode(obj); } } 

Eso fue bastante fácil, solo un comparador que obliga al uso de System.Object para obtener el hash y la igualdad de referencia … ahora viene el trabajo duro:

 private static T InternalClone(T input, Dictionary graph) { if (input == null || input is string || input.GetType().IsPrimitive) return input; Type inputType = input.GetType(); object exists; if (graph.TryGetValue(input, out exists)) return (T)exists; if (input is Array) { Array arItems = (Array)((Array)(object)input).Clone(); graph.Add(input, arItems); for (long ix = 0; ix < arItems.LongLength; ix++) arItems.SetValue(InternalClone(arItems.GetValue(ix), graph), ix); return (T)(object)arItems; } else if (input is Delegate) { Delegate original = (Delegate)(object)input; Delegate result = null; foreach (Delegate fn in original.GetInvocationList()) { Delegate fnNew; if (graph.TryGetValue(fn, out exists)) fnNew = (Delegate)exists; else { fnNew = Delegate.CreateDelegate(input.GetType(), InternalClone(original.Target, graph), original.Method, true); graph.Add(fn, fnNew); } result = Delegate.Combine(result, fnNew); } graph.Add(input, result); return (T)(object)result; } else { Object output = FormatterServices.GetUninitializedObject(inputType); if (!inputType.IsValueType) graph.Add(input, output); MemberInfo[] fields = inputType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); object[] values = FormatterServices.GetObjectData(input, fields); for (int i = 0; i < values.Length; i++) values[i] = InternalClone(values[i], graph); FormatterServices.PopulateObjectMembers(output, fields, values); return (T)output; } } 

Notará de inmediato el caso especial para la matriz y la copia delegada. Cada uno tiene sus propias razones, primero el Array no tiene 'miembros' que puedan clonarse, por lo que debe manejar esto y depender del miembro Clone () superficial y luego clonar cada elemento. En cuanto al delegado puede funcionar sin el caso especial; sin embargo, esto será mucho más seguro ya que no está duplicando cosas como RuntimeMethodHandle y similares. Si pretende incluir otras cosas en su jerarquía desde el tiempo de ejecución central (como System.Type), le sugiero que las maneje explícitamente de manera similar.

El último caso, y el más común, es simplemente usar aproximadamente las mismas rutinas que utiliza BinaryFormatter. Esto nos permite sacar todos los campos de instancia (públicos o privados) del objeto original, clonarlos y pegarlos en un objeto vacío. Lo bueno aquí es que GetUninitializedObject devuelve una nueva instancia que no ha ejecutado el ctor, lo que podría causar problemas y ralentizar el rendimiento.

Si lo anterior funciona o no dependerá en gran medida de su gráfico de objeto específico y los datos que contiene. Si controla los objetos en el gráfico y sabe que no hacen referencia a cosas tontas como un hilo, entonces el código anterior debería funcionar muy bien.

Pruebas:

Aquí está lo que escribí para probar esto originalmente:

 class Test { public Test(string name, params Test[] children) { Print = (Action)Delegate.Combine( new Action(delegate(StringBuilder sb) { sb.AppendLine(this.Name); }), new Action(delegate(StringBuilder sb) { sb.AppendLine(this.Name); }) ); Name = name; Children = children; } public string Name; public Test[] Children; public Action Print; } static void Main(string[] args) { Dictionary data2, data = new Dictionary(StringComparer.OrdinalIgnoreCase); Test a, b, c; data.Add("a", a = new Test("a", new Test("aa"))); a.Children[0].Children = new Test[] { a }; data.Add("b", b = new Test("b", a)); data.Add("c", c = new Test("c")); data2 = Clone(data); Assert.IsFalse(Object.ReferenceEquals(data, data2)); //basic contents test & comparer Assert.IsTrue(data2.ContainsKey("a")); Assert.IsTrue(data2.ContainsKey("A")); Assert.IsTrue(data2.ContainsKey("B")); //nodes are different between data and data2 Assert.IsFalse(Object.ReferenceEquals(data["a"], data2["a"])); Assert.IsFalse(Object.ReferenceEquals(data["a"].Children[0], data2["a"].Children[0])); Assert.IsFalse(Object.ReferenceEquals(data["B"], data2["B"])); Assert.IsFalse(Object.ReferenceEquals(data["B"].Children[0], data2["B"].Children[0])); Assert.IsFalse(Object.ReferenceEquals(data["B"].Children[0], data2["A"])); //graph intra-references still in tact? Assert.IsTrue(Object.ReferenceEquals(data["B"].Children[0], data["A"])); Assert.IsTrue(Object.ReferenceEquals(data2["B"].Children[0], data2["A"])); Assert.IsTrue(Object.ReferenceEquals(data["A"].Children[0].Children[0], data["A"])); Assert.IsTrue(Object.ReferenceEquals(data2["A"].Children[0].Children[0], data2["A"])); data2["A"].Name = "anew"; StringBuilder sb = new StringBuilder(); data2["A"].Print(sb); Assert.AreEqual("anew\r\nanew\r\n", sb.ToString()); } 

Nota final:

Honestamente fue un ejercicio divertido en ese momento. Generalmente es una gran cosa tener una clonación profunda en un modelo de datos. La realidad de hoy es que la mayoría de los modelos de datos se generan, lo que deja obsoleta la utilidad de la piratería de arriba con una rutina de clonación profunda. Recomiendo ampliamente generar su modelo de datos y su capacidad para realizar clones profundos en lugar de usar el código anterior.

La mejor forma es serializar su objeto y luego devolver la copia deserializada. Recogerá todo lo relacionado con su objeto, excepto los marcados como no serializables, y facilita la herencia de la serialización.

 [Serializable] public class ParentObj: ICloneable { private int myA; [NonSerialized] private object somethingInternal; public virtual object Clone() { MemoryStream ms = new MemoryStream(); BinaryFormatter formatter = new BinaryFormatter(); formatter.Serialize(ms, this); object clone = formatter.Deserialize(ms); return clone; } } [Serializable] public class ChildObj: ParentObj { private int myB; // No need to override clone, as it will still serialize the current object, including the new myB field } 

No es lo más destacado, pero tampoco lo es la alternativa: la relección. El beneficio de esta opción es que hereda perfectamente.

  1. Podría usar la reflexión para enlazar todas las variables y copiarlas. (Lento) si es lento para su software, puede usar DynamicMethod y generarlo.
  2. Serializar el objeto y deserializarlo de nuevo.

No creo que estés implementando ICloneable correctamente aquí; Requiere un método Clone () sin parámetros. Lo que recomendaría es algo como:

 public class ParentObj : ICloneable { public virtual Object Clone() { var obj = new ParentObj(); CopyObject(this, obj); } protected virtual CopyObject(ParentObj source, ParentObj dest) { dest.myA = source.myA; } } public class ChildObj : ParentObj { public override Object Clone() { var obj = new ChildObj(); CopyObject(this, obj); } public override CopyObject(ChildObj source, ParentObj dest) { base.CopyObject(source, dest) dest.myB = source.myB; } } 

Tenga en cuenta que CopyObject () es básicamente Object.MemberwiseClone (), presumiblemente haría más que solo copiar valores, también clonaría cualquier miembro que sea una clase.

Trate de usar lo siguiente [use la palabra clave “nuevo”]

 public class Parent { private int _X; public int X{ set{_X=value;} get{return _X;}} public Parent copy() { return new Parent{X=this.X}; } } public class Child:Parent { private int _Y; public int Y{ set{_Y=value;} get{return _Y;}} public new Child copy() { return new Child{X=this.X,Y=this.Y}; } } 

Debería usar el método MemberwiseClone lugar:

 public class ParentObj : ICloneable { protected int myA; public virtual Object Clone() { ParentObj newObj = this.MemberwiseClone() as ParentObj; newObj.myA = this.MyA; // not required, as value type (int) is automatically already duplicated. return newObj; } } public class ChildObj : ParentObj { protected int myB; public override Object Clone() { ChildObj newObj = base.Clone() as ChildObj; newObj.myB = this.MyB; // not required, as value type (int) is automatically already duplicated return newObj; } }