Copiar constructor versus Clonar ()


119

En C #, ¿cuál es la forma preferida de agregar funcionalidad de copia (profunda) a una clase? ¿Debe uno implementar el constructor de copia, o más bien derivar ICloneablee implementar elClone() método?

Observación : escribí "profundo" entre corchetes porque pensé que era irrelevante. Aparentemente, otros no están de acuerdo, así que pregunté si un constructor / operador / función de copia debe dejar en claro qué variante de copia implementa .

Respuestas:


91

No deberías derivar de ICloneable.

La razón es que cuando Microsoft diseñó el marco .net nunca especificó si el Clone()método en ICloneabledebería ser un clon profundo o superficial, por lo que la interfaz está semánticamente rota ya que las personas que llaman no sabrán si la llamada clonará el objeto en profundidad o superficial.

En su lugar, debe definir sus propias IDeepCloneable(y IShallowCloneable) interfaces con DeepClone()(y ShallowClone()) métodos.

Puede definir dos interfaces, una con un parámetro genérico para admitir la clonación fuertemente tipada y otra sin para mantener la capacidad de clonación débil para cuando está trabajando con colecciones de diferentes tipos de objetos clonables:

public interface IDeepCloneable
{
    object DeepClone();
}
public interface IDeepCloneable<T> : IDeepCloneable
{
    T DeepClone();
}

Que luego implementaría así:

public class SampleClass : IDeepCloneable<SampleClass>
{
    public SampleClass DeepClone()
    {
        // Deep clone your object
        return ...;
    }
    object IDeepCloneable.DeepClone()   
    {
        return this.DeepClone();
    }
}

Generalmente prefiero usar las interfaces descritas en lugar de un constructor de copias, mantiene la intención muy clara. Probablemente se supondría que un constructor de copias es un clon profundo, pero ciertamente no es una intención tan clara como usar una interfaz IDeepClonable.

Esto se discute en las Pautas de diseño de .net Framework y en el blog de Brad Abrams

(Supongo que si está escribiendo una aplicación (en lugar de un marco / biblioteca) para que pueda estar seguro de que nadie fuera de su equipo llamará a su código, no importa tanto y puede asignar un significado semántico de "deepclone" a la interfaz .net ICloneable, pero debe asegurarse de que esto esté bien documentado y bien entendido dentro de su equipo. Personalmente, me apegaría a las pautas del marco).


2
Si busca una interfaz, ¿qué tal tener DeepClone (de T) () y DeepClone (de T) (ficticio como T), los cuales devuelven T? La última sintaxis permitiría inferir T basándose en el argumento.
supercat

@supercat: ¿Estás diciendo que tienes un parámetro ficticio para que se pueda inferir el tipo? Supongo que es una opción. No estoy seguro de que me guste tener un parámetro ficticio solo para que el tipo se infiera automáticamente. Quizás te estoy entendiendo mal. (Tal vez publique algún código en una nueva respuesta para que pueda ver lo que quiere decir).
Simon P Stevens

@supercat: El parámetro ficticio existiría precisamente para permitir la inferencia de tipos. Hay situaciones en las que algún código puede querer clonar algo sin tener acceso a lo que es el tipo (por ejemplo, porque es un campo, propiedad o función devuelta desde otra clase) y un parámetro ficticio permitiría un medio de inferir correctamente el tipo. Pensándolo bien, probablemente no sea realmente útil, ya que el objetivo de una interfaz sería crear algo así como una colección que se pueda clonar en profundidad, en cuyo caso el tipo debería ser el tipo genérico de las colecciones.
supercat

2
¡Pregunta! ¿En qué situación querría alguna vez la versión no genérica? Para mí, tiene sentido que solo IDeepCloneable<T>exista, porque ... sabes qué T si haces tu propia implementación, es decirSomeClass : IDeepCloneable<SomeClass> { ... }
Kyle Baran

2
@Kyle dice que tenía un método que tomaba objetos clonables MyFunc(IDeepClonable data), entonces podría funcionar en todos los clonables, no solo en un tipo específico. O si tuvieras una colección de clonables. IEnumerable<IDeepClonable> lotsOfCloneablesentonces podrías clonar muchos objetos al mismo tiempo. Sin embargo, si no necesita ese tipo de cosas, deje el no genérico.
Simon P Stevens

33

En C #, ¿cuál es la forma preferida de agregar funcionalidad de copia (profunda) a una clase? ¿Debería uno implementar el constructor de copia, o más bien derivar de ICloneable e implementar el método Clone ()?

El problema con ICloneablees, como han mencionado otros, que no especifica si se trata de una copia profunda o superficial, lo que la hace prácticamente inutilizable y, en la práctica, poco utilizada. También vuelve object, lo cual es un fastidio, ya que requiere mucho yeso. (Y aunque mencionaste específicamente las clases en la pregunta, implementar ICloneableen un structrequiere boxeo).

Un constructor de copias también sufre uno de los problemas con ICloneable. No es obvio si un constructor de copias está haciendo una copia profunda o superficial.

Account clonedAccount = new Account(currentAccount); // Deep or shallow?

Lo mejor sería crear un método DeepClone (). De esta manera la intención es perfectamente clara.

Esto plantea la cuestión de si debería ser un método estático o de instancia.

Account clonedAccount = currentAccount.DeepClone();  // instance method

o

Account clonedAccount = Account.DeepClone(currentAccount); // static method

A veces prefiero un poco la versión estática, solo porque la clonación parece algo que se está haciendo en un objeto en lugar de algo que el objeto está haciendo. En cualquier caso, habrá problemas con los que lidiar al clonar objetos que son parte de una jerarquía de herencia, y cómo esos problemas pueden finalmente impulsar el diseño.

class CheckingAccount : Account
{
    CheckAuthorizationScheme checkAuthorizationScheme;

    public override Account DeepClone()
    {
        CheckingAccount clone = new CheckingAccount();
        DeepCloneFields(clone);
        return clone;
    }

    protected override void DeepCloneFields(Account clone)
    {
        base.DeepCloneFields(clone);

        ((CheckingAccount)clone).checkAuthorizationScheme = this.checkAuthorizationScheme.DeepClone();
    }
}

1
Aunque no sé si la opción DeepClone () es la mejor, me gusta mucho su respuesta, ya que subraya la situación confusa que existe acerca de una característica básica del lenguaje de programación en mi opinión. Supongo que depende del usuario elegir qué opción le gusta más.
Dimitri C.

11
No voy a discutir los puntos aquí, pero en mi opinión, la persona que llama no debería preocuparse tanto por lo profundo o lo superficial cuando llama a Clone (). Deben saber que están obteniendo un clon sin estado compartido no válido. Por ejemplo, es perfectamente posible que en un clon profundo, no quiera clonar en profundidad todos los elementos. Todo lo que debe preocuparle a la persona que llama a Clone es que están obteniendo una nueva copia que no tiene referencias inválidas y sin respaldo al original. Llamar al método "DeepClone" parece transmitir demasiados detalles de implementación a la persona que llama.
zumalifeguard

1
¿Qué tiene de malo que una instancia de objeto sepa cómo clonarse a sí misma en lugar de ser copiada por un método estático? Esto ocurre en el mundo real todo el tiempo con células biológicas. Las células de tu propio cuerpo están ocupadas clonándose a sí mismas en este momento mientras lees esto. En mi opinión, la opción del método estático es más engorrosa, tiende a ocultar la funcionalidad y se desvía del uso de la implementación "menos sorprendente" en beneficio de otros.
Ken Beckett

8
@KenBeckett - La razón por la que siento que la clonación es algo que se hace a un objeto es porque un objeto debe "hacer una cosa y hacerlo bien". Normalmente, hacer copias de sí mismo no es la competencia principal de una clase, sino que es una funcionalidad que se agrega. Hacer un clon de una cuenta bancaria es algo que muy bien puede querer hacer, pero hacer clones de sí mismo no es una característica de una cuenta bancaria. Su ejemplo celular no es muy instructivo, porque la reproducción es precisamente para lo que evolucionaron las células. Cell.Clone sería un buen método de instancia, pero esto no es cierto para la mayoría de las otras cosas.
Jeffrey L Whitledge

23

Recomiendo usar un constructor de copia sobre un método de clonación principalmente porque un método de clonación evitará que cree campos readonlyque podrían haber sido si hubiera usado un constructor en su lugar.

Si necesita la clonación polimórfica, puede agregar un método abstracto virtual Clone()a su clase base que implementa con una llamada al constructor de copia.

Si necesita más de un tipo de copia (por ejemplo: profunda / superficial), puede especificarla con un parámetro en el constructor de copia, aunque en mi experiencia encuentro que lo que necesito normalmente es una mezcla de copia profunda y superficial.

Ex:

public class BaseType {
   readonly int mBaseField;

   public BaseType(BaseType pSource) =>
      mBaseField = pSource.mBaseField;

   public virtual BaseType Clone() =>
      new BaseType(this);
}

public class SubType : BaseType {
   readonly int mSubField;

   public SubType(SubType pSource)
   : base(pSource) =>
      mSubField = pSource.mSubField;

   public override BaseType Clone() =>
      new SubType(this);
}

8
+1 Para abordar la clonación polimórfica; una aplicación significativa de la clonación.
samis

18

Existe un gran argumento de que debería implementar clone () usando un constructor de copia protegido

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 a una instancia de una clase en sí, proporcionando así extensibilidad y también, creando de forma segura los objetos usando el constructor de copia protegida.

Así que esta no es una pregunta "versus". Es posible que necesite tanto constructor (es) de copia como una interfaz de clonación para hacerlo bien.

(Aunque la interfaz pública recomendada es la interfaz Clone () en lugar de la basada en Constructor).

No se deje atrapar por el argumento explícito profundo o superficial de las otras respuestas. En el mundo real, casi siempre es algo intermedio y, de cualquier manera, no debería ser una preocupación para la persona que llama.

El contrato Clone () es simplemente "no cambiará cuando cambie el primero". La cantidad de gráfico que tienes que copiar o cómo evitas la recursividad infinita para que eso suceda no debería preocupar a la persona que llama.


"no debería ser preocupación de quien llama". No podría estar más de acuerdo pero, aquí estoy, tratando de averiguar si List <T> aList = new List <T> (aFullListOfT) hará una copia profunda (que es lo que quiero) o una copia superficial (que se rompería mi código) y si tengo que implementar otra forma de hacer el trabajo.
ThunderGr

3
Una lista <T> es demasiado genérica (ja, ja) para que la clonación tenga sentido. En su caso, eso ciertamente es solo una copia de la lista y NO los objetos apuntados por la lista. La manipulación de la nueva lista no afectará a la primera lista, pero los objetos son los mismos y, a menos que sean inmutables, los del primer conjunto cambiarán si cambia los del segundo conjunto. Si hubiera una operación list.Clone () en su biblioteca, debe esperar que el resultado sea un clon completo como en "no cambiará cuando haga algo con el primero". que también se aplica a los objetos contenidos.
DanO

1
Una List <T> no va a saber nada más sobre clonar correctamente sus contenidos que tú. Si el objeto subyacente es inmutable, está listo para comenzar. De lo contrario, si el objeto subyacente tiene un método Clone (), tendrá que usarlo. List <T> aList = new List <T> (aFullListOfT.Select (t = t.Clone ())
DanO

1
+1 para el enfoque híbrido. Ambos enfoques tienen una ventaja y una desventaja, pero esto parece tener el beneficio más general.
Kyle Baran

12

No se recomienda implementar ICloneable debido al hecho de que no se especifica si es una copia profunda o superficial, así que iría por el constructor o simplemente implementaría algo usted mismo. ¡Quizás lo llame DeepCopy () para que sea realmente obvio!


5
@ Grant, ¿cómo retransmite el constructor la intención? IOW, si un objeto se tomó a sí mismo en el constructor, ¿la copia es profunda o superficial? De lo contrario, estoy completamente de acuerdo con la sugerencia de DeepCopy () (o de otro modo).
Marc

7
Yo diría que un constructor es casi tan poco claro como la interfaz ICloneable; tendrías que leer los documentos / código de la API para saber si hace un clon profundo o no. Solo defino una IDeepCloneable<T>interfaz con un DeepClone()método.
Kent Boogaart

2
@Jon - ¡La reacción nunca ha terminado!
Grant Crofton

@Marc, @Kent: sí, justo, el constructor probablemente tampoco sea una buena idea.
Grant Crofton

3
¿Alguien ha visto un uso en el que iCloneable se usó en un objeto de tipo desconocido? El objetivo de las interfaces es que se pueden usar en objetos de tipo desconocido; de lo contrario, también se puede hacer que Clone sea un método estándar que devuelva el tipo en cuestión.
supercat

12

Te encontrarás con problemas con los constructores de copias y las clases abstractas. Imagina que quieres hacer lo siguiente:

abstract class A
{
    public A()
    {
    }

    public A(A ToCopy)
    {
        X = ToCopy.X;
    }
    public int X;
}

class B : A
{
    public B()
    {
    }

    public B(B ToCopy) : base(ToCopy)
    {
        Y = ToCopy.Y;
    }
    public int Y;
}

class C : A
{
    public C()
    {
    }

    public C(C ToCopy)
        : base(ToCopy)
    {
        Z = ToCopy.Z;
    }
    public int Z;
}

class Program
{
    static void Main(string[] args)
    {
        List<A> list = new List<A>();

        B b = new B();
        b.X = 1;
        b.Y = 2;
        list.Add(b);

        C c = new C();
        c.X = 3;
        c.Z = 4;
        list.Add(c);

        List<A> cloneList = new List<A>();

        //Won't work
        //foreach (A a in list)
        //    cloneList.Add(new A(a)); //Not this time batman!

        //Works, but is nasty for anything less contrived than this example.
        foreach (A a in list)
        {
            if(a is B)
                cloneList.Add(new B((B)a));
            if (a is C)
                cloneList.Add(new C((C)a));
        }
    }
}

Inmediatamente después de hacer lo anterior, comienza a desear haber usado una interfaz o haberse conformado con una implementación de DeepCopy () / ICloneable.Clone ().


2
Buen argumento para un enfoque basado en interfaces.
DanO

4

El problema con ICloneable es tanto la intención como la coherencia. Nunca está claro si es una copia profunda o superficial. Por eso, probablemente nunca se use de una manera u otra.

No encuentro un constructor de copia público que sea más claro al respecto.

Dicho esto, introduciría un sistema de métodos que funcione para usted y transmita la intención (algo que se autodocumenta)


3

Si el objeto que está intentando copiar es serializable, puede clonarlo serializándolo y deserializándolo. Entonces no es necesario escribir un constructor de copia para cada clase.

No tengo acceso al código en este momento, pero es algo como esto

public object DeepCopy(object source)
{
   // Copy with Binary Serialization if the object supports it
   // If not try copying with XML Serialization
   // If not try copying with Data contract Serailizer, etc
}

6
El uso de la serialización como medio de implementar la clonación profunda es irrelevante para la cuestión de si la clonación profunda debe surgir como un ctor o un método.
Kent Boogaart

1
Creo que es otra alternativa válida. No pensé que estuviera limitado a esos dos métodos de copia profunda.
Shaun Bowe

5
@Kent Boogaart - Dado que el OP comienza con la línea "En C #, cuál es la forma preferida de agregar funcionalidad de copia (profunda) a una clase", creo que es lo suficientemente justo que Shaun ofrezca diferentes alternativas. Especialmente en un escenario heredado en el que tiene una gran cantidad de clases para las que desea implementar la funcionalidad de clonación, este truco puede ser útil; no tan liviano como implementar su propio clon directamente, pero sin embargo es útil. Si la gente nunca hubiera ofrecido alternativas de "¿has pensado en ...?" A mis preguntas, no habría aprendido tanto como he aprendido a lo largo de los años.
Rob Levine

2

Depende de la semántica de copia de la clase en cuestión, que debe definir usted mismo como desarrollador. El método elegido generalmente se basa en los casos de uso previstos de la clase. Quizás tenga sentido implementar ambos métodos. Pero ambos comparten una desventaja similar: no está exactamente claro qué método de copia implementan. Esto debe estar claramente indicado en la documentación de su clase.

Para mi tener:

// myobj is some transparent proxy object
var state = new ObjectState(myobj.State);

// do something

myobject = GetInstance();
var newState = new ObjectState(myobject.State);

if (!newState.Equals(state))
    throw new Exception();

en vez de:

// myobj is some transparent proxy object
var state = myobj.State.Clone();

// do something

myobject = GetInstance();
var newState = myobject.State.Clone();

if (!newState.Equals(state))
    throw new Exception();

parecía una declaración de intenciones más clara.


0

Creo que debería haber un patrón estándar para objetos clonables, aunque no estoy seguro de cuál debería ser exactamente el patrón. Con respecto a la clonación, parece que hay tres tipos de clases:

  1. Aquellos que apoyan explícitamente la clonación profunda
  2. Aquellos en los que la clonación por miembros funcionará como clonación profunda, pero que no tienen ni necesitan apoyo explícito.
  3. Aquellos que no se pueden clonar en profundidad de manera útil y donde la clonación por miembros dará malos resultados.

Por lo que puedo decir, la única forma (al menos en .net 2.0) de obtener un nuevo objeto de la misma clase que un objeto existente es usar MemberwiseClone. Un buen patrón parece ser tener una función Clone "nueva" / "Shadows" que siempre devuelve el tipo actual, cuya definición es siempre llamar a MemberwiseClone y luego llamar a una subrutina virtual protegida CleanupClone (originalObject). La rutina CleanupCode debe llamar a base.Cleanupcode para manejar las necesidades de clonación del tipo base y luego agregar su propia limpieza. Si la rutina de clonación tiene que usar el objeto original, tendría que ser encasillado, pero de lo contrario, el único encasillado sería en la llamada de MemberwiseClone.

Desafortunadamente, el nivel más bajo de clase que era del tipo (1) anterior en lugar del tipo (2) tendría que codificarse para asumir que sus tipos más bajos no necesitarían ningún soporte explícito para la clonación. Realmente no veo ninguna forma de evitar eso.

Aún así, creo que tener un patrón definido sería mejor que nada.

Por cierto, si uno sabe que el tipo base de uno es compatible con iCloneable, pero no conoce el nombre de la función que utiliza, ¿hay alguna forma de hacer referencia a la función iCloneable.Clone del tipo base de uno?


0

Si lee todas las respuestas y discusiones interesantes, es posible que aún se pregunte cómo copia exactamente las propiedades, todas explícitamente, o ¿hay una forma más elegante de hacerlo? Si esa es su pregunta restante, eche un vistazo a esto (en StackOverflow):

¿Cómo puedo clonar "profundamente" las propiedades de clases de terceros utilizando un método de extensión genérico?

Describe cómo implementar un método de extensión CreateCopy()que crea una copia "profunda" del objeto que incluye todas las propiedades (sin tener que copiar propiedad por propiedad manualmente).

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.