Copia costruttore contro Clone ()


120

In C #, qual è il modo preferito per aggiungere funzionalità di copia (approfondita) a una classe? Si dovrebbe implementare il costruttore di copia, o piuttosto derivare ICloneablee implementare il Clone()metodo?

Nota : ho scritto "profondo" tra parentesi perché pensavo fosse irrilevante. Apparentemente altri non sono d'accordo, quindi ho chiesto se un costruttore / operatore / funzione di copia deve chiarire quale variante di copia implementa .

Risposte:


91

Non dovresti derivare da ICloneable.

Il motivo è che quando Microsoft ha progettato il framework .net non ha mai specificato se il Clone()metodo ICloneabledeve essere un clone profondo o superficiale, quindi l'interfaccia è semanticamente interrotta poiché i chiamanti non sapranno se la chiamata clonerà l'oggetto in modo profondo o superficiale.

Invece, si dovrebbe definire il proprio IDeepCloneable(e IShallowCloneable) interfacce con DeepClone()(e ShallowClone()metodi).

È possibile definire due interfacce, una con un parametro generico per supportare la clonazione fortemente tipizzata e l'altra senza per mantenere la capacità di clonazione debolmente tipizzata per quando si lavora con raccolte di diversi tipi di oggetti clonabili:

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

Che poi implementeresti in questo modo:

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

Generalmente preferisco usare le interfacce descritte al contrario di un costruttore di copie che mantiene l'intento molto chiaro. Si presume probabilmente che un costruttore di copie sia un clone profondo, ma non è certamente un intento chiaro come l'utilizzo di un'interfaccia IDeepClonable.

Questo è discusso nelle .net Framework Design Guidelines e nel blog di Brad Abrams

(Suppongo che se stai scrivendo un'applicazione (al contrario di un framework / libreria) così puoi essere sicuro che nessuno al di fuori del tuo team chiamerà il tuo codice, non importa così tanto e puoi assegnare un significato semantico di "clone in profondità" all'interfaccia .net ICloneable, ma dovresti assicurarti che sia ben documentato e ben compreso all'interno del tuo team. Personalmente mi atterrei alle linee guida del framework.)


2
Se stai cercando un'interfaccia, che ne dici di avere DeepClone (di T) () e DeepClone (di T) (fittizio come T), che restituiscono entrambi T? La seconda sintassi consentirebbe di inferire T in base all'argomento.
supercat

@supercat: Stai dicendo di avere un parametro fittizio in modo che il tipo possa essere dedotto? È un'opzione suppongo. Non sono sicuro che mi piaccia avere un parametro fittizio solo per ottenere il tipo dedotto automaticamente. Forse ti sto fraintendendo. (Forse inserisci del codice in una nuova risposta così posso vedere cosa intendi).
Simon P Stevens

@supercat: il parametro fittizio esisterebbe proprio per consentire l'inferenza del tipo. Ci sono situazioni in cui un po 'di codice potrebbe voler clonare qualcosa senza avere pronto accesso a ciò che è il tipo (ad esempio perché è un campo, una proprietà o una funzione restituita da un'altra classe) e un parametro fittizio consentirebbe un mezzo per inferire correttamente il tipo. A pensarci bene, probabilmente non è molto utile poiché il punto centrale di un'interfaccia sarebbe creare qualcosa come una raccolta clonabile in profondità, nel qual caso il tipo dovrebbe essere il tipo generico delle raccolte.
supercat

2
Domanda! In quale situazione vorresti mai la versione non generica? Per me, ha senso solo IDeepCloneable<T>esistere, perché ... sai cosa T se fai la tua implementazione, cioèSomeClass : IDeepCloneable<SomeClass> { ... }
Kyle Baran

2
@ Kyle dice che hai un metodo che prende oggetti clonabili MyFunc(IDeepClonable data), quindi potrebbe funzionare su tutti i clonabili, non solo su un tipo specifico. O se avevi una collezione di oggetti di cancelleria. IEnumerable<IDeepClonable> lotsOfCloneablesallora potresti clonare molti oggetti allo stesso tempo. Se non hai bisogno di quel genere di cose, lascia fuori quella non generica.
Simon P Stevens

33

In C #, qual è il modo preferito per aggiungere funzionalità di copia (approfondita) a una classe? Si dovrebbe implementare il costruttore di copia, o piuttosto derivare da ICloneable e implementare il metodo Clone ()?

Il problema con ICloneableè, come altri hanno detto, che non specifica se si tratta di una copia profonda o superficiale, il che la rende praticamente inutilizzabile e, in pratica, usata raramente. Inoltre ritorna object, il che è un dolore, poiché richiede molto lancio. (E sebbene tu abbia menzionato specificamente le classi nella domanda, l'implementazione ICloneablesu un structrichiede boxe.)

Un copy constuctor soffre anche di uno dei problemi con ICloneable. Non è ovvio se un costruttore di copie sta eseguendo una copia profonda o superficiale.

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

Sarebbe meglio creare un metodo DeepClone (). In questo modo l'intento è perfettamente chiaro.

Ciò solleva la questione se debba essere un metodo statico o di istanza.

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

o

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

A volte preferisco leggermente la versione statica, solo perché la clonazione sembra qualcosa che viene fatto su un oggetto piuttosto che qualcosa che sta facendo l'oggetto. In entrambi i casi, ci saranno problemi da affrontare durante la clonazione di oggetti che fanno parte di una gerarchia di ereditarietà e il modo in cui tali problemi vengono risolti potrebbe alla fine guidare il progetto.

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
Anche se non so se l'opzione DeepClone () sia la migliore, mi piace molto la tua risposta, in quanto sottolinea la situazione confusa che esiste su una caratteristica di base del linguaggio di programmazione a mio parere. Immagino che spetti all'utente scegliere quale opzione gli piace di più.
Dimitri C.

11
Non ho intenzione di discutere i punti qui, ma a mio parere, il chiamante non dovrebbe preoccuparsi così tanto di deep o shallow quando chiama Clone (). Dovrebbero sapere che stanno ottenendo un clone senza stato condiviso non valido. Ad esempio, è perfettamente possibile che in un clone profondo, potrei non voler clonare in profondità ogni elemento. Tutto ciò di cui il chiamante di Clone dovrebbe preoccuparsi è che sta ricevendo una nuova copia che non ha riferimenti non validi e non supportati all'originale. Chiamare il metodo "DeepClone" sembra trasmettere troppi dettagli di implementazione al chiamante.
zumalifeguard,

1
Cosa c'è di sbagliato in un'istanza di oggetto che sa come clonarsi invece di essere copiata da un metodo statico? Ciò si verifica nel mondo reale tutto il tempo con le cellule biologiche. Le cellule del tuo corpo sono impegnate a clonare se stesse proprio ora mentre leggi questo. IMO, l'opzione del metodo statico è più ingombrante, tende a nascondere la funzionalità e si discosta dall'utilizzo dell'implementazione "meno sorprendente" a beneficio degli altri.
Ken Beckett

8
@KenBeckett - Il motivo per cui ritengo che la clonazione sia qualcosa che viene fatto su un oggetto è perché un oggetto dovrebbe "fare una cosa e farla bene". Normalmente, fare copie di se stesso non è la competenza principale di una classe, ma piuttosto questa è la funzionalità che viene applicata. Fare un clone di un conto bancario è qualcosa che potresti desiderare di fare, ma creare cloni di se stesso non è una caratteristica di un conto bancario. Il tuo esempio cellulare non è ampiamente istruttivo, perché la riproduzione è esattamente ciò per cui le cellule si sono evolute. Cell.Clone sarebbe un buon metodo di istanza, ma questo non è vero per la maggior parte delle altre cose.
Jeffrey L Whitledge,

23

Consiglio di utilizzare un costruttore di copia su un metodo clone principalmente perché un metodo clone ti impedirà di creare campi readonlyche avrebbero potuto essere se avessi invece usato un costruttore.

Se è necessaria la clonazione polimorfica, è quindi possibile aggiungere un metodo abstracto virtual Clone()alla classe base che si implementa con una chiamata al costruttore di copia.

Se hai bisogno di più di un tipo di copia (es: profondo / superficiale) puoi specificarlo con un parametro nel costruttore di copia, anche se nella mia esperienza trovo che di solito un misto di copia profonda e superficiale è ciò di cui ho bisogno.

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 Per affrontare la clonazione polimorfica; una significativa applicazione della clonazione.
samis

18

C'è un ottimo argomento per implementare clone () usando un costruttore di copie protetto

È meglio fornire un costruttore di copia protetto (non pubblico) e richiamarlo dal metodo clone. Questo ci dà la possibilità di delegare il compito di creare un oggetto a un'istanza di una classe stessa, fornendo così estensibilità e anche, creando in sicurezza gli oggetti usando il costruttore di copia protetta.

Quindi questa non è una domanda "contro". Potresti aver bisogno di entrambi i costruttori di copia e un'interfaccia clone per farlo bene.

(Sebbene l'interfaccia pubblica consigliata sia l'interfaccia Clone () piuttosto che basata sul costruttore.)

Non lasciarti coinvolgere dall'argomento esplicito profondo o superficiale nelle altre risposte. Nel mondo reale è quasi sempre qualcosa di intermedio - e in entrambi i casi, non dovrebbe essere la preoccupazione del chiamante.

Il contratto Clone () è semplicemente "non cambierà quando cambio il primo". Quanta parte del grafico devi copiare, o come eviti la ricorsione infinita per farlo accadere non dovrebbe riguardare il chiamante.


"non dovrebbe essere la preoccupazione del chiamante". Non potrei essere più d'accordo ma, eccomi qui, cercando di capire se List <T> aList = new List <T> (aFullListOfT) farà una copia profonda (che è quello che voglio) o una copia superficiale (che si spezzerebbe il mio codice) e se devo implementare un altro modo per portare a termine il lavoro!
ThunderGr

3
Un elenco <T> è troppo generico (ah ah) perché il clone abbia senso. Nel tuo caso, questa è certamente solo una copia della lista e NON gli oggetti puntati dalla lista. La manipolazione del nuovo elenco non influirà sul primo elenco, ma gli oggetti sono gli stessi e, a meno che non siano immutabili, quelli del primo set cambieranno se cambi quelli del secondo set. Se ci fosse un'operazione list.Clone () nella tua libreria, dovresti aspettarti che il risultato sia un clone completo come in "non cambierà quando faccio qualcosa al primo". questo vale anche per gli oggetti contenuti.
DanO

1
Un List <T> non saprà nulla di più sulla clonazione corretta dei suoi contenuti di quanto lo siate voi. Se l'oggetto sottostante è immutabile, sei a posto. Altrimenti, se l'oggetto sottostante ha un metodo Clone () dovrai usarlo. List <T> aList = new List <T> (aFullListOfT.Select (t = t.Clone ())
DanO

1
+1 per l'approccio ibrido. Entrambi gli approcci hanno un vantaggio e uno svantaggio, ma questo sembra avere il vantaggio più generale.
Kyle Baran

12

L'implementazione di ICloneable non è consigliata poiché non è specificato se si tratta di una copia profonda o superficiale, quindi sceglierei il costruttore o implementerei qualcosa da solo. Forse chiamalo DeepCopy () per renderlo davvero ovvio!


5
@ Grant, in che modo il costruttore trasmette l'intento? IOW, se un oggetto ha preso se stesso nel costruttore, la copia è profonda o superficiale? In caso contrario, sono completamente d'accordo con il suggerimento di DeepCopy () (o altro).
Marc

7
Direi che un costruttore è poco chiaro quanto l'interfaccia ICloneable: dovresti leggere i documenti / codice dell'API per sapere che esegue un clone profondo o meno. Definisco semplicemente IDeepCloneable<T>un'interfaccia con un DeepClone()metodo.
Kent Boogaart,

2
@ Jon - Il reattore non è mai finito!
Grant Crofton

@ Marc, @ Kent - sì, punto equo, il costruttore probabilmente non è nemmeno una buona idea.
Grant Crofton

3
Qualcuno ha visto un uso in cui iCloneable è stato utilizzato su un oggetto di tipo sconosciuto? Il punto centrale delle interfacce è che possono essere utilizzate su oggetti di tipo sconosciuto; altrimenti si può anche semplicemente fare in modo che Clone sia un metodo standard che restituisce il tipo in questione.
supercat

12

Incontrerai problemi con i costruttori di copie e le classi astratte. Immagina di voler fare quanto segue:

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

Subito dopo aver fatto quanto sopra, inizi a desiderare di aver utilizzato un'interfaccia o di aver optato per un'implementazione di DeepCopy () / ICloneable.Clone ().


2
Buon argomento per un approccio basato sull'interfaccia.
DanO

4

Il problema con ICloneable è sia l'intento che la coerenza. Non è mai chiaro se si tratta di una copia profonda o superficiale. Per questo motivo, probabilmente non viene mai utilizzato solo in un modo o nell'altro.

Non trovo che un costruttore di copie pubbliche sia più chiaro su questo argomento.

Detto questo, introdurrei un sistema di metodi che funzioni per te e trasmetta l'intento (in qualche modo si documenta da solo)


3

Se l'oggetto che stai tentando di copiare è Serializable, puoi clonarlo serializzandolo e deserializzandolo. Quindi non è necessario scrivere un costruttore di copia per ogni classe.

Al momento non ho accesso al codice ma è qualcosa del genere

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
L'uso della serializzazione come mezzo per implementare la clonazione profonda è irrilevante per la questione se il clone profondo debba essere emerso come ctor o metodo.
Kent Boogaart,

1
Penso che sia un'altra valida alternativa. Non pensavo fosse limitato a quei due metodi di copia profonda.
Shaun Bowe,

5
@Kent Boogaart - Dato che l'OP inizia con la riga "In C #, qual è il modo preferito per aggiungere funzionalità di copia (profonda) a una classe", penso che sia abbastanza giusto per Shaun offrire diverse alternative. Soprattutto in uno scenario precedente in cui si dispone di un gran numero di classi per le quali si desidera implementare la funzionalità di clonazione, questo trucco può essere utile; non così leggero come implementare direttamente il tuo clone, ma comunque utile. Se le persone non avessero mai offerto "hai pensato a ..." alternative alle mie domande, non avrei imparato nemmeno tanto quanto ho imparato nel corso degli anni.
Rob Levine

2

Dipende dalla semantica di copia della classe in questione, che dovresti definire come sviluppatore. Il metodo scelto è generalmente basato sui casi d'uso previsti della classe. Forse avrà senso implementare entrambi i metodi. Ma entrambi condividono uno svantaggio simile: non è esattamente chiaro quale metodo di copia implementano. Questo dovrebbe essere chiaramente indicato nella documentazione della tua classe.

Per me avere:

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

invece di:

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

sembrava una dichiarazione di intenti più chiara.


0

Penso che dovrebbe esserci un modello standard per gli oggetti clonabili, anche se non sono sicuro di quale dovrebbe essere esattamente il modello. Per quanto riguarda la clonazione, sembrerebbe che esistano tre tipi di classi:

  1. Quelli che supportano esplicitamente la clonazione profonda
  2. Quelli in cui la clonazione per membro funzionerà come clonazione profonda, ma che non hanno né necessitano di supporto esplicito.
  3. Quelli che non possono essere clonati in modo proficuo e in cui la clonazione per membro darà risultati negativi.

Per quanto ne so, l'unico modo (almeno in .net 2.0) per ottenere un nuovo oggetto della stessa classe di un oggetto esistente è usare MemberwiseClone. Un bel pattern sembrerebbe essere quello di avere una funzione "new" / "Shadows" Clone che restituisce sempre il tipo attuale, la cui definizione è sempre quella di chiamare MemberwiseClone e quindi chiamare una subroutine virtuale protetta CleanupClone (originalObject). La routine CleanupCode dovrebbe chiamare base.Cleanupcode per gestire le esigenze di clonazione del tipo di base e quindi aggiungere la propria pulizia. Se la routine di clonazione deve utilizzare l'oggetto originale, dovrebbe essere typecast, ma altrimenti l'unico typecasting sarebbe sulla chiamata MemberwiseClone.

Sfortunatamente, il livello più basso di classe che era di tipo (1) sopra piuttosto che di tipo (2) dovrebbe essere codificato per presumere che i suoi tipi inferiori non necessitino di alcun supporto esplicito per la clonazione. Non vedo davvero alcun modo per aggirare questo.

Tuttavia, penso che avere uno schema definito sarebbe meglio di niente.

Per inciso, se si sa che il proprio tipo di base supporta iCloneable, ma non si conosce il nome della funzione che utilizza, c'è un modo per fare riferimento alla funzione iCloneable.Clone del proprio tipo di base?


0

Se leggi tutte le risposte e le discussioni interessanti, potresti ancora chiederti come copiare esattamente le proprietà, tutte in modo esplicito, o c'è un modo più elegante per farlo? Se questa è la tua domanda rimanente, dai un'occhiata a questa (su StackOverflow):

Come posso clonare "profondamente" le proprietà di classi di terze parti utilizzando un metodo di estensione generico?

Descrive come implementare un metodo di estensione CreateCopy()che crea una copia "profonda" dell'oggetto comprese tutte le proprietà (senza dover copiare manualmente proprietà per proprietà).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.