Salin konstruktor versus Klon ()


120

Di C #, apa cara yang disukai untuk menambahkan fungsionalitas salin (dalam) ke kelas? Haruskah seseorang menerapkan konstruktor salinan, atau lebih tepatnya berasal dari ICloneabledan mengimplementasikan Clone()metode?

Catatan : Saya menulis "dalam" di dalam tanda kurung karena saya pikir itu tidak relevan. Rupanya orang lain tidak setuju, jadi saya bertanya apakah salinan konstruktor / operator / fungsi perlu menjelaskan varian salinan mana yang diimplementasikannya .

Jawaban:


91

Anda tidak harus berasal dari ICloneable.

Alasannya adalah bahwa ketika Microsoft merancang kerangka kerja .net mereka tidak pernah menentukan apakah Clone()metode pada ICloneableharus klon yang dalam atau dangkal, sehingga antarmuka secara semantik rusak karena pemanggil Anda tidak akan tahu apakah panggilan akan mengkloning objek secara mendalam atau dangkal.

Sebagai gantinya, Anda harus mendefinisikan antarmuka Anda sendiri IDeepCloneable(dan IShallowCloneable) dengan metode DeepClone()(dan ShallowClone()).

Anda dapat menentukan dua antarmuka, satu dengan parameter umum untuk mendukung kloning yang diketik dengan kuat dan satu lagi tanpa untuk mempertahankan kemampuan kloning yang diketik dengan lemah saat Anda bekerja dengan koleksi berbagai jenis objek yang dapat digandakan:

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

Yang kemudian akan Anda terapkan seperti ini:

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

Umumnya saya lebih suka menggunakan antarmuka yang dijelaskan sebagai lawan dari konstruktor salinan itu membuat maksudnya sangat jelas. Konstruktor salinan mungkin akan dianggap sebagai klon yang dalam, tetapi maksudnya jelas tidak sebanyak menggunakan antarmuka IDeepClonable.

Ini dibahas dalam Pedoman Desain Kerangka .net dan di blog Brad Abrams

(Saya kira jika Anda menulis aplikasi (sebagai lawan dari kerangka kerja / pustaka) sehingga Anda dapat yakin tidak ada orang di luar tim Anda yang akan memanggil kode Anda, itu tidak terlalu penting dan Anda dapat menetapkan arti semantik dari "deepclone" ke antarmuka .net ICloneable, tetapi Anda harus memastikan ini didokumentasikan dengan baik dan dipahami dengan baik dalam tim Anda. Secara pribadi, saya akan tetap berpegang pada pedoman kerangka kerja.)


2
Jika Anda mencari antarmuka, bagaimana jika memiliki DeepClone (dari T) () dan DeepClone (dari T) (dummy as T), keduanya mengembalikan T? Sintaks terakhir akan memungkinkan T disimpulkan berdasarkan argumen.
supercat

@supercat: Apakah Anda mengatakan memiliki parameter dummy sehingga tipe dapat disimpulkan? Itu adalah opsi yang saya kira. Saya tidak yakin saya suka memiliki parameter dummy hanya untuk mendapatkan tipe yang disimpulkan secara otomatis. Mungkin saya salah paham tentang Anda. (Mungkin memposting beberapa kode dalam jawaban baru sehingga saya bisa mengerti maksud Anda).
Simon P Stevens

@supercat: Parameter dummy akan ada secara tepat untuk memungkinkan inferensi jenis. Ada situasi di mana beberapa kode mungkin ingin mengkloning sesuatu tanpa memiliki akses siap ke jenisnya (misalnya karena ini adalah bidang, properti, atau fungsi yang dikembalikan dari kelas lain) dan parameter dummy akan memungkinkan sarana untuk menyimpulkan jenis dengan tepat. Berpikir tentang itu, itu mungkin tidak terlalu membantu karena inti dari sebuah antarmuka adalah untuk membuat sesuatu seperti koleksi yang dapat di-cloneable, dalam hal ini jenisnya adalah tipe generik dari koleksi.
supercat

2
Pertanyaan! Dalam situasi apa Anda menginginkan versi non generik? Bagi saya, masuk akal untuk hanya IDeepCloneable<T>ada, karena ... Anda tahu apa T jika Anda membuat penerapan sendiri, yaituSomeClass : IDeepCloneable<SomeClass> { ... }
Kyle Baran

2
@Kyle mengatakan Anda memiliki metode yang mengambil objek yang dapat digandakan MyFunc(IDeepClonable data), kemudian dapat bekerja pada semua yang dapat digandakan, bukan hanya tipe tertentu. Atau jika Anda memiliki koleksi cloneable. IEnumerable<IDeepClonable> lotsOfCloneableslalu Anda dapat mengkloning banyak objek pada saat yang bersamaan. Jika Anda tidak membutuhkan hal semacam itu, tinggalkan yang non-generik.
Simon P Stevens

33

Di C #, apa cara yang disukai untuk menambahkan fungsionalitas salin (dalam) ke kelas? Haruskah seseorang menerapkan konstruktor salinan, atau lebih tepatnya berasal dari ICloneable dan mengimplementasikan metode Clone ()?

Masalahnya ICloneableadalah, seperti yang telah disebutkan orang lain, bahwa itu tidak menentukan apakah itu salinan yang dalam atau dangkal, yang membuatnya praktis tidak dapat digunakan dan, dalam praktiknya, jarang digunakan. Ini juga kembali object, yang merepotkan, karena membutuhkan banyak casting. (Dan meskipun Anda secara khusus menyebutkan kelas dalam pertanyaan tersebut, menerapkan ICloneablepada structtinju memerlukan.)

Copy constuctor juga mengalami salah satu masalah dengan ICloneable. Tidak jelas apakah konstruktor salinan melakukan salinan dalam atau dangkal.

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

Akan lebih baik untuk membuat metode DeepClone (). Dengan cara ini maksudnya sangat jelas.

Ini menimbulkan pertanyaan apakah itu harus menjadi metode statis atau contoh.

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

atau

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

Kadang-kadang saya lebih suka versi statis, hanya karena kloning tampak seperti sesuatu yang dilakukan pada suatu objek daripada sesuatu yang dilakukan objek. Dalam kedua kasus tersebut, akan ada masalah yang harus ditangani saat mengkloning objek yang merupakan bagian dari hierarki pewarisan, dan bagaimana masalah tersebut dihapus pada akhirnya dapat mendorong desain.

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
Meskipun saya tidak tahu apakah opsi DeepClone () adalah yang terbaik, saya sangat menyukai jawaban Anda, karena ini menggarisbawahi situasi membingungkan yang ada tentang fitur bahasa pemrograman dasar menurut pendapat saya. Saya rasa terserah pengguna untuk memilih opsi mana yang paling dia sukai.
Dimitri C.

11
Saya tidak akan memperdebatkan poin-poinnya di sini, tetapi menurut saya, penelepon tidak boleh terlalu peduli tentang dalam atau dangkal ketika mereka memanggil Clone (). Mereka harus tahu bahwa mereka mendapatkan klon tanpa status bersama yang tidak valid. Misalnya, sangat mungkin bahwa dalam klon yang dalam, saya mungkin tidak ingin mengkloning setiap elemen secara mendalam. Semua yang harus diperhatikan oleh pemanggil Clone adalah bahwa mereka mendapatkan salinan baru yang tidak memiliki referensi yang tidak valid dan tidak didukung ke aslinya. Memanggil metode 'DeepClone "tampaknya menyampaikan terlalu banyak detail implementasi kepada pemanggil.
zumalifeguard

1
Apa yang salah dengan contoh objek yang mengetahui cara mengkloning dirinya sendiri alih-alih disalin dengan metode statis? Ini terjadi di dunia nyata sepanjang waktu dengan sel biologis. Sel-sel di tubuh Anda sendiri sedang sibuk mengkloning dirinya sendiri saat Anda membaca ini. IMO, opsi metode statis lebih rumit, cenderung menyembunyikan fungsionalitas, dan menyimpang dari penggunaan implementasi yang "paling tidak mengejutkan" untuk kepentingan orang lain.
Ken Beckett

8
@KenBeck - Alasan saya merasa bahwa kloning adalah sesuatu yang dilakukan pada suatu objek adalah karena objek harus "melakukan satu hal dan melakukannya dengan baik." Biasanya, membuat salinan dirinya sendiri bukanlah kompetensi inti dari sebuah kelas, melainkan fungsionalitas yang diterapkan. Membuat klon dari BankAccount adalah sesuatu yang mungkin ingin Anda lakukan, tetapi membuat klon sendiri bukanlah fitur dari rekening bank. Contoh sel Anda tidak bersifat instruktif secara luas, karena reproduksi persis seperti yang dilakukan sel berevolusi. Cell.Clone akan menjadi metode contoh yang baik, tetapi ini tidak berlaku untuk kebanyakan hal lainnya.
Jeffrey L Whitledge

23

Saya merekomendasikan menggunakan konstruktor salinan melalui metode klon terutama karena metode klon akan mencegah Anda membuat bidang readonlyyang bisa jadi jika Anda menggunakan konstruktor sebagai gantinya.

Jika Anda memerlukan kloning polimorfik, Anda kemudian dapat menambahkan metode abstractatau virtual Clone()ke kelas dasar yang Anda implementasikan dengan panggilan ke konstruktor salinan.

Jika Anda memerlukan lebih dari satu jenis salinan (misal: dalam / dangkal) Anda dapat menentukannya dengan parameter di konstruktor salinan, meskipun menurut pengalaman saya, saya menemukan bahwa biasanya campuran penyalinan dalam dan dangkal adalah yang saya butuhkan.

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 Untuk menangani kloning polimorfik; aplikasi kloning yang signifikan.
samis

18

Ada argumen bagus bahwa Anda harus mengimplementasikan clone () menggunakan konstruktor salinan yang dilindungi

Lebih baik menyediakan konstruktor salinan yang dilindungi (non-publik) dan memanggilnya dari metode klon. Ini memberi kita kemampuan untuk mendelegasikan tugas membuat objek ke instance kelas itu sendiri, sehingga memberikan ekstensibilitas dan juga, dengan aman membuat objek menggunakan konstruktor salinan yang dilindungi.

Jadi ini bukanlah pertanyaan "versus". Anda mungkin memerlukan konstruktor salinan dan antarmuka kloning untuk melakukannya dengan benar.

(Meskipun antarmuka publik yang disarankan adalah antarmuka Clone () daripada berbasis Konstruktor.)

Jangan terjebak dalam argumen mendalam atau dangkal yang eksplisit di jawaban lain. Dalam dunia nyata, ini hampir selalu ada di antara keduanya - dan bagaimanapun, seharusnya tidak menjadi perhatian penelepon.

Kontrak Clone () hanya "tidak akan berubah ketika saya mengubah yang pertama". Berapa banyak grafik yang harus Anda salin, atau bagaimana Anda menghindari rekursi tak terbatas untuk mewujudkannya seharusnya tidak menjadi perhatian pemanggil.


"seharusnya tidak menjadi perhatian penelepon". Saya sangat setuju, tetapi, inilah saya, mencoba mencari tahu apakah List <T> aList = new List <T> (aFullListOfT) akan melakukan deep copy (yang saya inginkan) atau Copy dangkal (yang akan merusak kode saya) dan apakah saya harus menerapkan cara lain untuk menyelesaikan pekerjaan!
ThunderGr

3
Daftar <T> terlalu umum (ha ha) untuk dikloning bahkan tidak masuk akal. Dalam kasus Anda, itu pasti hanya salinan dari daftar dan BUKAN objek yang ditunjukkan oleh daftar. Memanipulasi daftar baru tidak akan memengaruhi daftar pertama, tetapi objeknya sama dan kecuali mereka tetap, objek di set pertama akan berubah jika Anda mengubah yang di set kedua. Jika ada operasi list.Clone () di perpustakaan Anda, Anda harus mengharapkan hasilnya berupa klon lengkap seperti "tidak akan berubah ketika saya melakukan sesuatu ke yang pertama". yang berlaku untuk objek yang terkandung juga.
DanO

1
List <T> tidak akan tahu apa-apa tentang mengkloning kontennya dengan benar daripada Anda. Jika objek yang mendasarinya tidak dapat diubah, Anda siap melakukannya. Jika tidak, jika objek yang mendasari memiliki metode Clone () Anda harus menggunakannya. Daftar <T> aList = Daftar baru <T> (aFullListOfT.Select (t = t.Clone ())
DanO

1
1 untuk pendekatan hybrid. Kedua pendekatan tersebut memiliki keuntungan dan kerugian, tetapi ini tampaknya memiliki manfaat yang lebih menyeluruh.
Kyle Baran

12

Menerapkan ICloneable tidak disarankan karena fakta bahwa itu tidak ditentukan apakah itu salinan yang dalam atau dangkal, jadi saya akan memilih konstruktor, atau hanya mengimplementasikan sesuatu sendiri. Mungkin menyebutnya DeepCopy () untuk membuatnya sangat jelas!


5
@ Grant, bagaimana maksud konstruktor menyampaikan? IOW, jika sebuah objek mengambil dirinya sendiri di konstruktor, apakah salinannya dalam atau dangkal? Jika tidak, saya setuju sepenuhnya dengan saran DeepCopy () (atau sebaliknya).
Marc

7
Saya berpendapat bahwa konstruktor hampir sama tidak jelasnya dengan antarmuka ICloneable - Anda harus membaca dokumen / kode API untuk mengetahui bahwa itu melakukan klon yang dalam atau tidak. Saya hanya mendefinisikan IDeepCloneable<T>antarmuka dengan DeepClone()metode.
Kent Boogaart

2
@Jon - Reaktornya tidak pernah selesai!
Grant Crofton

@Marc, @Kent - ya wajar saja, konstruktornya mungkin juga bukan ide yang bagus.
Grant Crofton

3
Adakah yang pernah melihat penggunaan di mana iCloneable digunakan pada objek dengan tipe yang tidak diketahui? Inti dari antarmuka adalah bahwa mereka dapat digunakan pada objek dengan tipe yang tidak diketahui; jika tidak, seseorang juga dapat membuat Clone menjadi metode standar yang mengembalikan jenis yang dimaksud.
supercat

12

Anda akan mengalami masalah dengan konstruktor salinan dan kelas abstrak. Bayangkan Anda ingin melakukan hal berikut:

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

Tepat setelah melakukan hal di atas, Anda mulai berharap Anda akan menggunakan antarmuka, atau menetap untuk implementasi DeepCopy () / ICloneable.Clone ().


2
Argumen yang bagus untuk pendekatan berbasis Antarmuka.
DanO

4

Masalah dengan ICloneable adalah niat dan konsistensi. Tidak pernah jelas apakah itu salinan yang dalam atau dangkal. Karena itu, mungkin tidak pernah digunakan hanya dengan satu cara atau lainnya.

Saya tidak menemukan konstruktor salinan publik untuk menjadi lebih jelas tentang masalah itu.

Yang mengatakan, saya akan memperkenalkan sistem metode yang bekerja untuk Anda dan menyampaikan maksud (a'la agak mendokumentasikan sendiri)


3

Jika objek yang Anda coba salin berseri, Anda dapat mengkloningnya dengan membuat serial dan deserialisasi. Maka Anda tidak perlu menulis konstruktor salinan untuk setiap kelas.

Saya tidak memiliki akses ke kode sekarang tetapi ini adalah sesuatu seperti ini

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
Penggunaan serialisasi sebagai sarana untuk menerapkan kloning yang dalam tidak relevan dengan pertanyaan apakah klon yang dalam harus dimunculkan sebagai ctor atau metode.
Kent Boogaart

1
Saya pikir ini adalah alternatif lain yang valid. Saya tidak berpikir dia terbatas pada dua metode penyalinan mendalam itu.
Shaun Bowe

5
@Kent Boogaart - Mengingat OP dimulai dengan baris "Dalam C #, cara apa yang disukai untuk menambahkan fungsionalitas salin (dalam) ke kelas", menurut saya cukup adil bagi Shaun untuk menawarkan alternatif yang berbeda. Terutama dalam skenario lama di mana Anda memiliki banyak kelas yang ingin Anda terapkan fungsionalitas klonnya, trik ini dapat berguna; tidak seringan menerapkan klon Anda sendiri secara langsung, namun tetap berguna. Jika orang tidak pernah menawarkan alternatif "pernahkah Anda memikirkan ..." untuk pertanyaan saya, saya tidak akan belajar sebanyak yang saya pelajari selama bertahun-tahun.
Rob Levine

2

Ini bergantung pada semantik salinan kelas yang dipermasalahkan, yang harus Anda definisikan sendiri sebagai pengembang. Metode yang dipilih biasanya didasarkan pada kasus penggunaan kelas yang dimaksudkan. Mungkin masuk akal untuk menerapkan kedua metode tersebut. Tetapi keduanya memiliki kelemahan yang sama - tidak jelas metode penyalinan mana yang mereka terapkan. Ini harus dinyatakan dengan jelas dalam dokumentasi untuk kelas Anda.

Bagi saya memiliki:

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

dari pada:

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

tampak sebagai pernyataan niat yang lebih jelas.


0

Saya pikir harus ada pola standar untuk objek yang dapat dikloning, meskipun saya tidak yakin apa sebenarnya pola itu. Berkenaan dengan kloning, terlihat ada tiga jenis kelas:

  1. Mereka yang secara eksplisit mendukung kloning yang dalam
  2. Kloning yang bersifat memberwise akan berfungsi sebagai kloning mendalam, tetapi tidak memiliki atau memerlukan dukungan eksplisit.
  3. Kloning yang tidak dapat dikloning secara berguna, dan kloning sesuai anggota akan menghasilkan hasil yang buruk.

Sejauh yang saya tahu, satu-satunya cara (setidaknya dalam .net 2.0) untuk mendapatkan objek baru dari kelas yang sama sebagai objek yang ada adalah dengan menggunakan MemberwiseClone. Pola yang bagus tampaknya memiliki fungsi "baru" / "Bayangan" Klon yang selalu mengembalikan tipe saat ini, yang definisinya selalu memanggil MemberwiseClone dan kemudian memanggil CleanupClone subrutin virtual yang dilindungi (originalObject). Rutin CleanupCode harus memanggil base.Cleanupcode untuk menangani kebutuhan kloning tipe dasar dan kemudian menambahkan pembersihannya sendiri. Jika rutinitas kloning harus menggunakan objek asli, itu harus berupa typecast, tetapi jika tidak, satu-satunya typecasting akan berada di panggilan MemberwiseClone.

Sayangnya, tingkat kelas terendah yang berjenis (1) di atas daripada jenis (2) harus dikodekan untuk mengasumsikan bahwa jenis yang lebih rendah tidak memerlukan dukungan eksplisit untuk kloning. Saya tidak benar-benar melihat jalan keluarnya.

Namun, saya pikir memiliki pola yang ditentukan akan lebih baik daripada tidak sama sekali.

Kebetulan, jika seseorang mengetahui bahwa jenis dasar seseorang mendukung iCloneable, tetapi tidak mengetahui nama fungsi yang digunakannya, apakah ada cara untuk mereferensikan fungsi iCloneable.Clone dari jenis dasar seseorang?


0

Jika Anda membaca semua jawaban dan diskusi yang menarik, Anda mungkin masih bertanya pada diri sendiri bagaimana tepatnya Anda menyalin properti - semuanya secara eksplisit, atau adakah cara yang lebih elegan untuk melakukannya? Jika itu adalah pertanyaan Anda yang tersisa, lihat ini (di StackOverflow):

Bagaimana saya bisa "sangat" mengkloning properti kelas pihak ketiga menggunakan metode ekstensi umum?

Ini menjelaskan cara mengimplementasikan metode ekstensi CreateCopy()yang membuat salinan "dalam" dari objek termasuk semua properti (tanpa harus menyalin properti per properti secara manual).

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.