Konstruktor kopieren versus Klon ()


119

Was ist in C # die bevorzugte Methode, um einer Klasse (Deep) Copy-Funktionen hinzuzufügen? Sollte man den Kopierkonstruktor implementieren oder vielmehr von ICloneableder Clone()Methode ableiten und diese implementieren ?

Bemerkung : Ich habe "tief" in Klammern geschrieben, weil ich dachte, dass es irrelevant ist. Anscheinend sind andere anderer Meinung, also fragte ich, ob ein Kopierkonstruktor / -operator / eine Kopierfunktion klarstellen muss, welche Kopiervariante er implementiert .

Antworten:


91

Sie sollten nicht ableiten von ICloneable.

Der Grund dafür ist, dass Microsoft beim Entwurf des .net-Frameworks nie angegeben hat, ob die Clone()Methode ICloneableein tiefer oder flacher Klon sein soll. Daher ist die Schnittstelle semantisch fehlerhaft, da Ihre Anrufer nicht wissen, ob der Aufruf das Objekt tief oder flach klonen wird.

Stattdessen sollten Sie Ihre eigenen IDeepCloneable(und IShallowCloneable) Schnittstellen mit DeepClone()(und ShallowClone()) Methoden definieren.

Sie können zwei Schnittstellen definieren, eine mit einem generischen Parameter zur Unterstützung des stark typisierten Klonens und eine ohne die schwach typisierte Klonfunktion beizubehalten, wenn Sie mit Sammlungen verschiedener Arten klonbarer Objekte arbeiten:

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

Was Sie dann so implementieren würden:

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

Im Allgemeinen bevorzuge ich die beschriebenen Schnittstellen im Gegensatz zu einem Kopierkonstruktor, der die Absicht sehr klar hält. Es wird wahrscheinlich angenommen, dass ein Kopierkonstruktor ein tiefer Klon ist, aber es ist sicherlich nicht so eindeutig wie die Verwendung einer IDeepClonable-Schnittstelle.

Dies wird in den .net Framework Design Guidelines und im Blog von Brad Abrams erläutert

(Ich nehme an, wenn Sie eine Anwendung schreiben (im Gegensatz zu einem Framework / einer Bibliothek), damit Sie sicher sein können, dass niemand außerhalb Ihres Teams Ihren Code aufruft, spielt dies keine Rolle und Sie können eine semantische Bedeutung von zuweisen "deepclone" auf die .net ICloneable-Schnittstelle, aber Sie sollten sicherstellen, dass dies in Ihrem Team gut dokumentiert und verstanden ist. Persönlich würde ich mich an die Framework-Richtlinien halten.)


2
Wenn Sie sich für eine Schnittstelle entscheiden, wie wäre es dann mit DeepClone (von T) () und DeepClone (von T) (Dummy als T), die beide T zurückgeben? Die letztere Syntax würde es ermöglichen, T basierend auf dem Argument abzuleiten.
Supercat

@supercat: Wollen Sie damit sagen, dass Sie einen Dummy-Parameter haben, damit der Typ abgeleitet werden kann? Es ist eine Option, nehme ich an. Ich bin mir nicht sicher, ob ich einen Dummy-Parameter mag, nur um den Typ automatisch abzuleiten. Vielleicht verstehe ich dich falsch. (Vielleicht poste einen Code in einer neuen Antwort, damit ich sehen kann, was du meinst).
Simon P Stevens

@supercat: Der Dummy-Parameter würde genau existieren, um eine Typinferenz zu ermöglichen. Es gibt Situationen, in denen ein Code möglicherweise etwas klonen möchte, ohne sofort auf den Typ zugreifen zu können (z. B. weil es sich um ein Feld, eine Eigenschaft oder eine Funktion handelt, die von einer anderen Klasse zurückgegeben werden), und ein Dummy-Parameter eine Möglichkeit bietet, den Typ ordnungsgemäß abzuleiten. Wenn man darüber nachdenkt, ist es wahrscheinlich nicht wirklich hilfreich, da der Sinn einer Schnittstelle darin besteht, so etwas wie eine tief klonbare Sammlung zu erstellen. In diesem Fall sollte der Typ der generische Typ der Sammlung sein.
Supercat

2
Frage! In welcher Situation würden Sie jemals die nicht generische Version wollen? Für mich ist es sinnvoll, nur IDeepCloneable<T>zu existieren, weil ... Sie wissen, was T ist, wenn Sie Ihre eigene Implementierung vornehmen, dhSomeClass : IDeepCloneable<SomeClass> { ... }
Kyle Baran

2
@Kyle sagt, Sie hatten eine Methode, die klonbare Objekte nahm MyFunc(IDeepClonable data), dann konnte sie auf allen klonbaren Objekten funktionieren, nicht nur auf einem bestimmten Typ. Oder wenn Sie eine Sammlung von Cloneables hatten. IEnumerable<IDeepClonable> lotsOfCloneablesDann können Sie viele Objekte gleichzeitig klonen. Wenn Sie so etwas nicht brauchen, lassen Sie das nicht generische weg.
Simon P Stevens

33

Was ist in C # die bevorzugte Methode, um einer Klasse (Deep) Copy-Funktionen hinzuzufügen? Sollte man den Kopierkonstruktor implementieren oder vielmehr von ICloneable ableiten und die Clone () -Methode implementieren?

Das Problem dabei ICloneableist, wie andere bereits erwähnt haben, dass nicht angegeben wird, ob es sich um eine tiefe oder flache Kopie handelt, was sie praktisch unbrauchbar macht und in der Praxis selten verwendet. Es kehrt auch zurück object, was ein Schmerz ist, da es viel Casting erfordert. (Und obwohl Sie in der Frage ausdrücklich Klassen erwähnt haben, erfordert die Implementierung ICloneableauf einem structBoxen.)

Ein Kopierkonstruktor leidet auch unter einem der Probleme mit ICloneable. Es ist nicht offensichtlich, ob ein Kopierkonstruktor eine tiefe oder flache Kopie erstellt.

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

Am besten erstellen Sie eine DeepClone () -Methode. Auf diese Weise ist die Absicht völlig klar.

Dies wirft die Frage auf, ob es sich um eine statische oder eine Instanzmethode handeln soll.

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

oder

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

Ich bevorzuge manchmal leicht die statische Version, nur weil das Klonen eher etwas ist, das mit einem Objekt gemacht wird, als etwas, das das Objekt macht. In beiden Fällen gibt es Probleme, die beim Klonen von Objekten, die Teil einer Vererbungshierarchie sind, behoben werden müssen, und wie diese Probleme behandelt werden, kann letztendlich das Design beeinflussen.

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
Obwohl ich nicht weiß, ob die DeepClone () -Option am besten ist, gefällt mir Ihre Antwort sehr gut, da sie die verwirrende Situation unterstreicht, die meiner Meinung nach bei einer grundlegenden Programmiersprachenfunktion besteht. Ich denke, es liegt an dem Benutzer, zu entscheiden, welche Option ihm am besten gefällt.
Dimitri C.

11
Ich werde hier nicht auf die Punkte eingehen, aber meiner Meinung nach sollte sich der Anrufer nicht so sehr um tief oder flach kümmern, wenn er Clone () aufruft. Sie sollten wissen, dass sie einen Klon ohne ungültigen gemeinsamen Status erhalten. Zum Beispiel ist es durchaus möglich, dass ich in einem tiefen Klon nicht jedes Element tief klonen möchte. Der Anrufer von Clone sollte sich nur darum kümmern, dass er eine neue Kopie erhält, die keine ungültigen und nicht unterstützten Verweise auf das Original enthält. Das Aufrufen der Methode 'DeepClone' scheint dem Aufrufer zu viele Implementierungsdetails zu vermitteln.
zumalifeguard

1
Was ist falsch daran, dass eine Objektinstanz weiß, wie sie sich selbst klonen kann, anstatt von einer statischen Methode kopiert zu werden? Dies geschieht in der realen Welt ständig mit biologischen Zellen. Zellen in Ihrem eigenen Körper sind gerade damit beschäftigt, sich selbst zu klonen, während Sie dies lesen. IMO, die statische Methodenoption, ist umständlicher, verbirgt die Funktionalität und weicht davon ab, die "am wenigsten überraschende" Implementierung zum Nutzen anderer zu verwenden.
Ken Beckett

8
@ KenBeckett - Der Grund, warum ich denke, dass das Klonen etwas ist, das mit einem Objekt gemacht wird, ist, dass ein Objekt "eine Sache tun und es gut machen sollte". Normalerweise ist das Erstellen von Kopien von sich selbst nicht die Kernkompetenz einer Klasse, sondern die Funktionalität, die in Angriff genommen wird. Das Erstellen eines Klons eines Bankkontos ist etwas, das Sie möglicherweise tun möchten, aber das Erstellen von Klonen von sich selbst ist keine Funktion eines Bankkontos. Ihr Zellbeispiel ist nicht allgemein aufschlussreich, da die Reproduktion genau das ist, wozu sich Zellen entwickelt haben. Cell.Clone wäre eine gute Instanzmethode, aber dies gilt nicht für die meisten anderen Dinge.
Jeffrey L Whitledge

23

Ich empfehle die Verwendung eines Kopierkonstruktors gegenüber einer Klonmethode in erster Linie, da eine Klonmethode Sie daran hindert, Felder zu erstellen readonly, die hätten erstellt werden können, wenn Sie stattdessen einen Konstruktor verwendet hätten.

Wenn Sie ein polymorphes Klonen benötigen, können Sie Ihrer Basisklasse eine abstractoder virtual Clone()-Methode hinzufügen , die Sie mit einem Aufruf des Kopierkonstruktors implementieren.

Wenn Sie mehr als eine Art von Kopie benötigen (z. B. tief / flach), können Sie diese mit einem Parameter im Kopierkonstruktor angeben, obwohl ich meiner Erfahrung nach normalerweise eine Mischung aus tiefem und flachem Kopieren benötige.

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 Zur Adressierung des polymorphen Klonens; eine bedeutende Anwendung des Klonens.
Samis

18

Es gibt ein gutes Argument, dass Sie clone () mit einem geschützten Kopierkonstruktor implementieren sollten

Es ist besser, einen geschützten (nicht öffentlichen) Kopierkonstruktor bereitzustellen und diesen über die Klonmethode aufzurufen. Dies gibt uns die Möglichkeit, die Aufgabe des Erstellens eines Objekts an eine Instanz einer Klasse selbst zu delegieren, wodurch Erweiterbarkeit bereitgestellt und die Objekte mithilfe des geschützten Kopierkonstruktors sicher erstellt werden.

Dies ist also keine "versus" Frage. Möglicherweise benötigen Sie sowohl Kopierkonstruktoren als auch eine Klonschnittstelle, um dies richtig zu machen.

(Obwohl die empfohlene öffentliche Schnittstelle die Clone () - Schnittstelle ist und nicht auf Konstruktoren basiert.)

Lassen Sie sich nicht in das explizite tiefe oder flache Argument in den anderen Antworten verwickeln. In der realen Welt ist es fast immer etwas dazwischen - und sollte auf keinen Fall das Anliegen des Anrufers sein.

Der Clone () -Vertrag lautet einfach "wird sich nicht ändern, wenn ich den ersten ändere". Wie viel des Diagramms Sie kopieren müssen oder wie Sie eine unendliche Rekursion vermeiden, um dies zu erreichen, sollte den Anrufer nicht betreffen.


"sollte nicht das Anliegen des Anrufers sein". Ich könnte nicht mehr zustimmen, aber hier versuche ich herauszufinden, ob List <T> aList = new List <T> (aFullListOfT) eine tiefe Kopie (was ich will) oder eine flache Kopie (die brechen würde) erstellt mein Code) und ob ich einen anderen Weg implementieren muss, um die Arbeit zu erledigen!
ThunderGr

3
Eine Liste <T> ist zu allgemein (ha ha), als dass ein Klon überhaupt einen Sinn ergeben könnte. In Ihrem Fall ist dies sicherlich nur eine Kopie der Liste und NICHT der Objekte, auf die die Liste zeigt. Das Bearbeiten der neuen Liste wirkt sich nicht auf die erste Liste aus, aber die Objekte sind dieselben. Wenn sie nicht unveränderlich sind, ändern sich die Objekte im ersten Satz, wenn Sie die Objekte im zweiten Satz ändern. Wenn in Ihrer Bibliothek eine list.Clone () -Operation vorhanden war, sollten Sie erwarten, dass das Ergebnis ein vollständiger Klon ist, wie in "Wird sich nicht ändern, wenn ich etwas mit dem ersten mache". das gilt auch für die enthaltenen objekte.
DanO

1
Eine Liste <T> weiß nichts mehr über das ordnungsgemäße Klonen ihres Inhalts als Sie. Wenn das zugrunde liegende Objekt unveränderlich ist, können Sie loslegen. Wenn das zugrunde liegende Objekt über eine Clone () -Methode verfügt, müssen Sie diese verwenden. Liste <T> aList = neue Liste <T> (aFullListOfT.Select (t = t.Clone ())
DanO

1
+1 für den Hybridansatz. Beide Ansätze haben Vor- und Nachteile, aber dies scheint den allgemeineren Nutzen zu haben.
Kyle Baran

12

Die Implementierung von ICloneable wird nicht empfohlen , da nicht angegeben ist, ob es sich um eine tiefe oder eine flache Kopie handelt. Daher würde ich mich für den Konstruktor entscheiden oder einfach selbst etwas implementieren. Nennen Sie es vielleicht DeepCopy (), um es wirklich offensichtlich zu machen!


5
@Grant, wie beabsichtigt der Konstruktor Relay? IOW, wenn sich ein Objekt im Konstruktor befindet, ist die Kopie tief oder flach? Ansonsten stimme ich dem Vorschlag von DeepCopy () (oder auf andere Weise) vollständig zu.
Marc

7
Ich würde argumentieren, dass ein Konstruktor fast so unklar ist wie die ICloneable-Schnittstelle - Sie müssten die API-Dokumente / den API-Code lesen, um zu wissen, ob er einen tiefen Klon ausführt oder nicht. Ich definiere nur eine IDeepCloneable<T>Schnittstelle mit einer DeepClone()Methode.
Kent Boogaart

2
@ Jon - Die Reaktorierung ist nie beendet!
Grant Crofton

@Marc, @Kent - ja, der Konstruktor ist wahrscheinlich auch keine gute Idee.
Grant Crofton

3
Hat jemand eine Verwendung gesehen, bei der iCloneable für ein Objekt unbekannten Typs verwendet wurde? Der springende Punkt bei Schnittstellen ist, dass sie für Objekte unbekannten Typs verwendet werden können. Andernfalls kann man Clone auch einfach zu einer Standardmethode machen, die den fraglichen Typ zurückgibt.
Supercat

12

Sie werden auf Probleme mit Kopierkonstruktoren und abstrakten Klassen stoßen. Stellen Sie sich vor, Sie möchten Folgendes tun:

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

Unmittelbar nach dem oben Gesagten wünschen Sie sich, Sie hätten entweder eine Schnittstelle verwendet oder sich für eine DeepCopy () / ICloneable.Clone () -Implementierung entschieden.


2
Gutes Argument für einen Interface-basierten Ansatz.
DanO

4

Das Problem mit ICloneable ist sowohl Absicht als auch Konsistenz. Es ist nie klar, ob es sich um eine tiefe oder flache Kopie handelt. Aus diesem Grund wird es wahrscheinlich nie nur auf die eine oder andere Weise verwendet.

Ich finde keinen öffentlichen Kopierkonstruktor, der in dieser Angelegenheit klarer ist.

Trotzdem würde ich ein Methodensystem einführen, das für Sie funktioniert und Absichten weiterleitet (a'la etwas selbstdokumentierend)


3

Wenn das Objekt, das Sie kopieren möchten, serialisierbar ist, können Sie es klonen, indem Sie es serialisieren und deserialisieren. Dann müssen Sie nicht für jede Klasse einen Kopierkonstruktor schreiben.

Ich habe momentan keinen Zugriff auf den Code, aber es ist ungefähr so

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
Die Verwendung der Serialisierung als Mittel zur Implementierung des Deep Cloning ist für die Frage, ob der Deep Clone als Ctor oder als Methode auftauchen soll, irrelevant.
Kent Boogaart

1
Ich denke, es ist eine weitere gültige Alternative. Ich dachte nicht, dass er auf diese beiden Methoden des tiefen Kopierens beschränkt war.
Shaun Bowe

5
@ Kent Boogaart - Angesichts der Tatsache, dass das OP mit der Zeile "In C #, was ist die bevorzugte Methode, um einer Klasse (tiefe) Kopierfunktionen hinzuzufügen" beginnt, halte ich es für fair genug, dass Shaun verschiedene Alternativen anbietet. Insbesondere in einem Legacy-Szenario, in dem Sie über eine große Anzahl von Klassen verfügen, für die Sie die Klonfunktion implementieren möchten, kann dieser Trick hilfreich sein. nicht so leicht wie die direkte Implementierung eines eigenen Klons, aber dennoch nützlich. Wenn die Leute niemals Alternativen zu meinen Fragen angeboten hätten, hätten Sie nicht annähernd so viel gelernt wie im Laufe der Jahre.
Rob Levine

2

Dies hängt von der Kopiersemantik der betreffenden Klasse ab, die Sie selbst als Entwickler definieren sollten. Die gewählte Methode basiert normalerweise auf den beabsichtigten Anwendungsfällen der Klasse. Vielleicht ist es sinnvoll, beide Methoden zu implementieren. Beide haben jedoch einen ähnlichen Nachteil: Es ist nicht genau klar, welche Kopiermethode sie implementieren. Dies sollte in der Dokumentation Ihrer Klasse klar angegeben werden.

Für mich mit:

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

anstatt:

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

sah als klarere Absichtserklärung aus.


0

Ich denke, es sollte ein Standardmuster für klonbare Objekte geben, obwohl ich nicht sicher bin, wie genau das Muster sein soll. In Bezug auf das Klonen scheint es drei Arten von Klassen zu geben:

  1. Diejenigen, die ausdrücklich das tiefe Klonen unterstützen
  2. Diejenigen, bei denen das Klonen von Mitgliedern erfolgt, funktionieren als tiefes Klonen, die jedoch keine explizite Unterstützung haben oder benötigen.
  3. Diejenigen, die nicht sinnvoll tief geklont werden können und bei denen das Klonen auf Mitgliedsebene zu schlechten Ergebnissen führt.

Soweit ich das beurteilen kann, besteht die einzige Möglichkeit (zumindest in .net 2.0), ein neues Objekt derselben Klasse wie ein vorhandenes Objekt zu erhalten, darin, MemberwiseClone zu verwenden. Ein schönes Muster scheint ein "neuer" / "Schatten" -Funktionsklon zu sein, der immer den aktuellen Typ zurückgibt, dessen Definition immer darin besteht, MemberwiseClone aufzurufen und dann eine geschützte virtuelle Unterroutine CleanupClone (originalObject) aufzurufen. Die CleanupCode-Routine sollte base.Cleanupcode aufrufen, um die Klonanforderungen des Basistyps zu erfüllen, und dann eine eigene Bereinigung hinzufügen. Wenn die Klonroutine das ursprüngliche Objekt verwenden muss, muss es typisiert werden, andernfalls wäre die einzige typisierte Übertragung der MemberwiseClone-Aufruf.

Leider müsste die niedrigste Klassenstufe, die vom Typ (1) oben und nicht vom Typ (2) war, codiert werden, um anzunehmen, dass ihre niedrigeren Typen keine explizite Unterstützung für das Klonen benötigen würden. Daran sehe ich keinen Weg vorbei.

Trotzdem denke ich, ein definiertes Muster wäre besser als nichts.

Wenn man weiß, dass der Basistyp iCloneable unterstützt, aber den Namen der verwendeten Funktion nicht kennt, gibt es dann eine Möglichkeit, auf die iCloneable.Clone-Funktion des Basistyps zu verweisen?


0

Wenn Sie alle interessanten Antworten und Diskussionen durchlesen, fragen Sie sich möglicherweise immer noch, wie genau Sie die Eigenschaften kopieren - alle explizit, oder gibt es eine elegantere Möglichkeit, dies zu tun? Wenn dies Ihre verbleibende Frage ist, schauen Sie sich diese an (bei StackOverflow):

Wie kann ich die Eigenschaften von Klassen von Drittanbietern mithilfe einer generischen Erweiterungsmethode „tief“ klonen?

Es wird beschrieben, wie eine Erweiterungsmethode implementiert wird, mit CreateCopy()der eine "tiefe" Kopie des Objekts einschließlich aller Eigenschaften erstellt wird (ohne dass Eigenschaft für Eigenschaft manuell kopiert werden muss).

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.