Konstruktor kopiujący kontra Clone ()


120

W języku C #, jaki jest preferowany sposób dodawania (głębokiej) funkcji kopiowania do klasy? Czy należy implementować konstruktor kopiujący, czy raczej wyprowadzać ICloneablei implementować Clone()metodę?

Uwaga : napisałem „głęboko” w nawiasach, ponieważ uważałem, że to nie ma znaczenia. Najwyraźniej inni się z tym nie zgadzają, więc zapytałem, czy konstruktor / operator / funkcja kopiowania musi wyjaśnić, który wariant kopii implementuje .

Odpowiedzi:


91

Nie powinieneś pochodzić z ICloneable.

Powodem jest to, że kiedy Microsoft zaprojektował framework .net nigdy nie określił, czy Clone()metoda ICloneablepowinna być głębokim, czy płytkim klonem, więc interfejs jest semantycznie zepsuty, ponieważ wywołujący nie będą wiedzieć, czy wywołanie będzie głęboko, czy płytko sklonuje obiekt.

Zamiast tego powinieneś zdefiniować własne IDeepCloneable(i IShallowCloneable) interfejsy z metodami DeepClone()(i ShallowClone()).

Można zdefiniować dwa interfejsy, jeden z parametrem ogólnym obsługującym klonowanie silnie wpisane, a drugi bez zachowania możliwości klonowania słabo typizowanego podczas pracy z kolekcjami różnych typów obiektów, które można klonować:

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

Które następnie zaimplementowałbyś w ten sposób:

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

Generalnie wolę używać opisanych interfejsów w przeciwieństwie do konstruktora kopiującego, dzięki czemu cel jest bardzo jasny. Prawdopodobnie zakłada się, że konstruktor kopiujący jest głębokim klonem, ale z pewnością nie jest to tak wyraźny zamiar, jak użycie interfejsu IDeepClonable.

Jest to omówione w wytycznych dotyczących projektowania frameworka .net oraz na blogu Brada Abramsa

(Przypuszczam, że jeśli piszesz aplikację (w przeciwieństwie do frameworka / biblioteki), więc możesz być pewien, że nikt spoza Twojego zespołu nie będzie wywoływał Twojego kodu, nie ma to większego znaczenia i możesz przypisać znaczenie semantyczne "deepclone" do interfejsu .net ICloneable, ale powinieneś upewnić się, że jest to dobrze udokumentowane i dobrze zrozumiane w Twoim zespole. Osobiście trzymałbym się wytycznych dotyczących frameworka).


2
Jeśli wybierasz interfejs, co powiesz na DeepClone (z T) () i DeepClone (z T) (atrapa jako T), które zwracają T? Ta ostatnia składnia pozwoliłaby wywnioskować T na podstawie argumentu.
supercat

@supercat: Czy chcesz powiedzieć, że masz parametr fikcyjny, aby można było wywnioskować typ? Przypuszczam, że to opcja. Nie jestem pewien, czy lubię mieć fikcyjny parametr tylko po to, aby automatycznie wywnioskować typ. Może cię nie rozumiem. (Może opublikuj jakiś kod w nowej odpowiedzi, abym mógł zobaczyć, co masz na myśli).
Simon P Stevens

@supercat: parametr fikcyjny istniałby właśnie po to, aby umożliwić wnioskowanie o typie. Są sytuacje, w których jakiś kod może chcieć sklonować coś bez szybkiego dostępu do tego, jaki jest typ (np. Ponieważ jest to pole, właściwość lub funkcja zwracana z innej klasy), a fikcyjny parametr pozwoliłby na prawidłowe wywnioskowanie typu. Myśląc o tym, prawdopodobnie nie jest to zbyt pomocne, ponieważ celem interfejsu byłoby stworzenie czegoś w rodzaju kolekcji, którą można głęboko klonować, w takim przypadku typem powinien być typ ogólny kolekcji.
supercat

2
Pytanie! W jakiej sytuacji chciałbyś mieć wersję nieogólną? Dla mnie to ma sens tylko IDeepCloneable<T>istnieć, bo ... wiesz co T, jeśli tworzysz własną implementację, czyliSomeClass : IDeepCloneable<SomeClass> { ... }
Kyle Baran

2
@Kyle mówi, że masz metodę, która pobierała klonowalne obiekty, MyFunc(IDeepClonable data)a następnie mogłaby działać na wszystkich klonowalnych elementach, a nie tylko na określonym typie. Lub jeśli masz kolekcję klonowalnych. IEnumerable<IDeepClonable> lotsOfCloneableswtedy możesz sklonować wiele obiektów jednocześnie. Jeśli jednak nie potrzebujesz tego rodzaju rzeczy, zostaw to, co nie jest ogólne.
Simon P Stevens,

33

W języku C #, jaki jest preferowany sposób dodawania (głębokiej) funkcji kopiowania do klasy? Czy należy zaimplementować konstruktor kopiujący, czy raczej wyprowadzić z ICloneable i zaimplementować metodę Clone ()?

Problem ICloneablepolega na tym, jak wspominali inni, że nie precyzuje, czy jest to głęboka, czy płytka kopia, co sprawia, że ​​jest praktycznie bezużyteczna iw praktyce rzadko używana. Wraca również object, co jest uciążliwe, ponieważ wymaga wielu rzutów. (I chociaż wyraźnie wspomniałeś o klasach w pytaniu, implementacja ICloneablena a structwymaga boksu.)

Konstruktor kopii również cierpi na jeden z problemów z ICloneable. Nie jest oczywiste, czy konstruktor kopiujący wykonuje głęboką, czy płytką kopię.

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

Najlepiej byłoby utworzyć metodę DeepClone (). W ten sposób zamiar jest doskonale jasny.

W związku z tym pojawia się pytanie, czy powinna to być metoda statyczna czy instancyjna.

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

lub

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

Nieco wolę czasami wersję statyczną, ponieważ klonowanie wydaje się być czymś, co jest wykonywane na obiekcie, a nie czymś, co robi obiekt. W obu przypadkach będą problemy do rozwiązania podczas klonowania obiektów, które są częścią hierarchii dziedziczenia, a sposób, w jaki te problemy są rozwiązywane, może ostatecznie wpłynąć na projekt.

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
Chociaż nie wiem, czy opcja DeepClone () jest najlepsza, bardzo podoba mi się twoja odpowiedź, ponieważ podkreśla ona zagmatwaną sytuację, która moim zdaniem istnieje w podstawowej funkcji języka programowania. Wydaje mi się, że użytkownik decyduje, która opcja najbardziej mu się podoba.
Dimitri C.

11
Nie zamierzam tutaj dyskutować, ale moim zdaniem dzwoniący nie powinien przejmować się tak bardzo głębokim lub płytkim, gdy wywołuje Clone (). Powinni wiedzieć, że otrzymują klon bez nieprawidłowego stanu wspólnego. Na przykład jest całkowicie możliwe, że w głębokim klonie mogę nie chcieć głęboko klonować każdego elementu. Jedyne, na czym dzwoniący Clone powinien dbać, to to, że otrzymuje nową kopię, która nie ma żadnych nieprawidłowych i nieobsługiwanych odniesień do oryginału. Wywołanie metody „DeepClone” wydaje się przekazywać dzwoniącemu zbyt wiele szczegółów implementacji.
zumalifeguard

1
Co jest złego w instancji obiektu, która wie, jak sklonować samą siebie, zamiast być kopiowaną metodą statyczną? Dzieje się to w prawdziwym świecie przez cały czas z komórkami biologicznymi. Komórki w twoim ciele są teraz zajęte klonowaniem się, kiedy to czytasz. IMO, opcja metody statycznej jest bardziej uciążliwa, skłania do ukrycia funkcjonalności i odbiega od stosowania „najmniej zaskakującej” implementacji z korzyścią dla innych.
Ken Beckett

8
@KenBeckett - Powodem, dla którego uważam, że klonowanie jest czymś, co robi się na obiekcie, jest to, że obiekt powinien „robić jedną rzecz i robić to dobrze”. Zwykle tworzenie kopii samego siebie nie jest podstawową kompetencją klasy, ale raczej jest to funkcjonalność, która jest dołączana. Tworzenie klonu konta bankowego to coś, co bardzo dobrze możesz chcieć zrobić, ale tworzenie klonów samego siebie nie jest cechą konta bankowego. Twój przykład komórki nie jest ogólnie pouczający, ponieważ reprodukcja jest dokładnie tym, do czego komórki ewoluowały. Cell.Clone byłaby dobrą metodą instancji, ale nie dotyczy to większości innych rzeczy.
Jeffrey L Whitledge,

23

Zalecam używanie konstruktora kopiującego readonlyzamiast metody clone przede wszystkim dlatego, że metoda clone uniemożliwi tworzenie pól, które mogłyby powstać, gdyby zamiast tego używałeś konstruktora.

Jeśli potrzebujesz klonowania polimorficznego, możesz dodać metodę abstractlub virtual Clone()do swojej klasy bazowej, którą implementujesz za pomocą wywołania konstruktora kopiującego.

Jeśli potrzebujesz więcej niż jednego rodzaju kopii (np: głębokie / płytkie), możesz określić to za pomocą parametru w konstruktorze kopiowania, chociaż z mojego doświadczenia wynika, że ​​zwykle potrzebuję mieszanki głębokiego i płytkiego kopiowania.

Dawny:

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 Do rozwiązywania problemów z klonowaniem polimorficznym; znaczące zastosowanie klonowania.
samis

18

Jest świetny argument, że powinieneś zaimplementować clone () używając konstruktora chronionego kopiowania

Lepiej jest zapewnić chroniony (niepubliczny) konstruktor kopiujący i wywołać go z metody clone. Daje nam to możliwość delegowania zadania tworzenia obiektu do instancji samej klasy, zapewniając w ten sposób rozszerzalność, a także bezpieczne tworzenie obiektów przy użyciu konstruktora chronionej kopii.

Nie jest to więc kwestia „przeciw”. Aby zrobić to dobrze, możesz potrzebować zarówno konstruktora kopiującego, jak i interfejsu klonowania.

(Chociaż zalecanym interfejsem publicznym jest interfejs Clone (), a nie oparty na konstruktorze).

Nie daj się wciągnąć w wyraźną, głęboką lub płytką argumentację w innych odpowiedziach. W prawdziwym świecie prawie zawsze jest to coś pośredniego - i tak czy inaczej, nie powinno to być problemem osoby dzwoniącej.

Kontrakt Clone () to po prostu „nie zmieni się, kiedy zmienię pierwszy”. To, ile wykresu musisz skopiować lub jak uniknąć nieskończonej rekurencji, aby tak się stało, nie powinno dotyczyć dzwoniącego.


„nie powinno być problemem dzwoniącego”. Nie mogłem się bardziej zgodzić, ale tutaj próbuję dowiedzieć się, czy List <T> aList = new List <T> (aFullListOfT) zrobi głęboką kopię (co jest tym, czego chcę), czy płytką kopię (która zepsuje mój kod) i czy muszę wdrożyć inny sposób, aby wykonać zadanie!
ThunderGr

3
Lista <T> jest zbyt ogólna (ha ha), aby klon w ogóle miała sens. W twoim przypadku jest to z pewnością tylko kopia listy, a NIE obiekty wskazane na liście. Manipulowanie nową listą nie wpłynie na pierwszą listę, ale obiekty są takie same i jeśli nie są niezmienne, te z pierwszego zestawu zmienią się, jeśli zmienisz te z drugiego zestawu. Gdyby w twojej bibliotece istniała operacja list.Clone (), powinieneś oczekiwać, że wynikiem będzie pełny klon, jak w przypadku „nie zmieni się, gdy zrobię coś z pierwszą”. dotyczy to również zawartych obiektów.
DanO

1
List <T> nie będzie wiedział nic więcej o poprawnym klonowaniu swojej zawartości niż ty. Jeśli podstawowy obiekt jest niezmienny, możesz iść. W przeciwnym razie, jeśli obiekt bazowy ma metodę Clone (), będziesz musiał jej użyć. List <T> aList = new List <T> (aFullListOfT.Select (t = t.Clone ())
DanO

1
+1 za podejście hybrydowe. Oba podejścia mają zalety i wady, ale wydaje się, że mają one bardziej ogólne korzyści.
Kyle Baran

12

Wdrażanie ICloneable nie jest zalecane , ponieważ nie jest określone, czy jest to głęboka, czy płytka kopia, więc wybrałbym konstruktora lub po prostu zaimplementował coś samodzielnie. Może nazwij to DeepCopy (), aby było to naprawdę oczywiste!


5
@Grant, jaki jest zamiar przekaźnika konstruktora? IOW, jeśli obiekt wszedł do konstruktora, czy kopia jest głęboka czy płytka? W przeciwnym razie zgadzam się całkowicie z sugestią DeepCopy () (lub w inny sposób).
Marc

7
Twierdzę, że konstruktor jest prawie tak niejasny, jak interfejs ICloneable - musiałbyś przeczytać dokumentację / kod API, aby wiedzieć, że wykonuje głębokie klonowanie, czy nie. Po prostu definiuję IDeepCloneable<T>interfejs za pomocą DeepClone()metody.
Kent Boogaart

2
@Jon - Reaktorowanie nigdy się nie skończyło!
Grant Crofton

@Marc, @Kent - tak słuszna uwaga, konstruktor też prawdopodobnie nie jest dobrym pomysłem.
Grant Crofton

3
Czy ktoś widział zastosowanie, w którym iCloneable był używany na obiekcie nieznanego typu? Istotą interfejsów jest to, że można ich używać na obiektach nieznanego typu; w przeciwnym razie równie dobrze można po prostu ustawić Clone jako metodę standardową, która zwraca dany typ.
supercat

12

Napotkasz problemy z konstruktorami kopiującymi i klasami abstrakcyjnymi. Wyobraź sobie, że chcesz wykonać następujące czynności:

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

Zaraz po wykonaniu powyższego zaczynasz żałować, że nie użyłeś interfejsu lub zdecydowałeś się na implementację DeepCopy () / ICloneable.Clone ().


2
Dobry argument za podejściem opartym na interfejsie.
DanO

4

Problem z ICloneable dotyczy zarówno intencji, jak i konsekwencji. Nigdy nie jest jasne, czy jest to głęboka, czy płytka kopia. Z tego powodu prawdopodobnie nigdy nie jest używany tylko w taki czy inny sposób.

Nie widzę publicznego konstruktora kopiującego, który byłby bardziej zrozumiały w tej sprawie.

To powiedziawszy, przedstawiłbym system metod, który działa dla ciebie i przekazuje zamiar (a'la trochę samodokumentujący)


3

Jeśli obiekt, który próbujesz skopiować, można serializować, możesz go sklonować, serializując go i deserializując. Wtedy nie musisz pisać konstruktora kopiującego dla każdej klasy.

Nie mam teraz dostępu do kodu, ale to jest coś takiego

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
Użycie serializacji jako sposobu implementacji głębokiego klonowania jest nieistotne dla pytania, czy głęboki klon powinien zostać ujawniony jako ktor czy metoda.
Kent Boogaart

1
Myślę, że to kolejna ważna alternatywa. Nie sądziłem, że był ograniczony do tych dwóch metod głębokiego kopiowania.
Shaun Bowe

5
@Kent Boogaart - Biorąc pod uwagę, że OP zaczyna się od wiersza „W języku C #, jaki jest preferowany sposób dodawania (głębokiej) funkcji kopiowania do klasy”, myślę, że jest wystarczająco uczciwe, aby Shaun zaoferował różne alternatywy. Ta sztuczka może być przydatna zwłaszcza w starszym scenariuszu, w którym masz dużą liczbę klas, dla których chcesz zaimplementować funkcję klonowania; nie jest tak lekki jak bezpośrednie wdrażanie własnego klonu, ale mimo to przydatny. Gdyby ludzie nigdy nie oferowali odpowiedzi typu „czy myślałeś o…” na moje pytania, nie nauczyłbym się prawie tak dużo, jak przez lata.
Rob Levine,

2

Zależy to od semantyki kopiowania danej klasy, którą powinieneś zdefiniować jako programistę. Wybrana metoda jest zwykle oparta na zamierzonych przypadkach użycia klasy. Może będzie miało sens wdrożenie obu metod. Ale oba mają podobną wadę - nie jest do końca jasne, jaką metodę kopiowania stosują. Powinno to być jasno określone w dokumentacji twojej klasy.

Dla mnie posiadający:

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

zamiast:

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

wyglądał jak jaśniejsze określenie intencji.


0

Myślę, że powinien istnieć standardowy wzorzec dla obiektów, które można klonować, chociaż nie jestem pewien, jaki dokładnie powinien być wzór. Jeśli chodzi o klonowanie, wydaje się, że istnieją trzy rodzaje klas:

  1. Te, które wyraźnie wspierają głębokie klonowanie
  2. Te, w których klonowanie członków będzie działać jako klonowanie głębokie, ale które nie mają ani nie potrzebują wyraźnego wsparcia.
  3. Te, których nie można z pożytkiem głęboko sklonować, a klonowanie oparte na członkach przyniesie złe wyniki.

O ile wiem, jedynym sposobem (przynajmniej w .net 2.0), aby uzyskać nowy obiekt tej samej klasy, co istniejący obiekt, jest użycie MemberwiseClone. Wydawałoby się, że fajnym wzorcem byłoby posiadanie "nowej" / "Shadows" funkcji Clone, która zawsze zwraca obecny typ, którego definicja polega zawsze na wywołaniu MemberwiseClone, a następnie wywołaniu chronionej wirtualnej procedury CleanupClone (originalObject). Procedura CleanupCode powinna wywołać base.Cleanupcode, aby obsłużyć potrzeby klonowania typu podstawowego, a następnie dodać własne czyszczenie. Jeśli procedura klonowania musi używać oryginalnego obiektu, musiałaby to być rzutowanie typu, ale w przeciwnym razie jedyne rzutowanie typów byłoby na wywołaniu MemberwiseClone.

Niestety, najniższy poziom klasy, który był typu (1) powyżej, a nie typu (2), musiałby być zakodowany, aby założyć, że jego niższe typy nie będą wymagały żadnego wyraźnego wsparcia dla klonowania. Naprawdę nie widzę sposobu na obejście tego.

Mimo to myślę, że posiadanie określonego wzoru byłoby lepsze niż nic.

Nawiasem mówiąc, jeśli ktoś wie, że jego typ podstawowy obsługuje iCloneable, ale nie zna nazwy funkcji, której używa, czy istnieje sposób na odwołanie się do funkcji iCloneable.Clone swojego typu podstawowego?


0

Jeśli przeczytasz wszystkie interesujące odpowiedzi i dyskusje, nadal możesz zadać sobie pytanie, w jaki sposób dokładnie kopiujesz właściwości - wszystkie wyraźnie, czy jest na to bardziej elegancki sposób? Jeśli to jest Twoje pozostałe pytanie, spójrz na to (w StackOverflow):

Jak mogę „głęboko” sklonować właściwości klas innych firm przy użyciu ogólnej metody rozszerzenia?

Opisuje, jak zaimplementować metodę rozszerzenia, CreateCopy()która tworzy „głęboką” kopię obiektu zawierającą wszystkie właściwości (bez konieczności ręcznego kopiowania właściwości według właściwości).

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.