Sao chép hàm tạo so với Clone ()


119

Trong C #, cách ưa thích để thêm chức năng sao chép (sâu) vào một lớp là gì? Có nên thực hiện các constructor sao chép, hoặc xuất phát từ ICloneablevà thực hiện Clone()phương thức?

Ghi chú : Tôi đã viết "sâu" trong ngoặc vì tôi nghĩ nó không liên quan. Rõ ràng những người khác không đồng ý, vì vậy tôi đã hỏi liệu một hàm tạo / toán tử / hàm sao chép có cần phải làm rõ biến thể sao chép mà nó thực hiện không .

Câu trả lời:


91

Bạn không nên xuất phát từ ICloneable.

Lý do là khi Microsoft thiết kế khung .net, họ không bao giờ chỉ định Clone()phương thức trên ICloneablelà bản sao sâu hay nông, do đó giao diện bị phá vỡ về mặt ngữ nghĩa vì người gọi của bạn sẽ không biết liệu cuộc gọi sẽ nhân bản sâu hay nông.

Thay vào đó, bạn nên xác định riêng của bạn IDeepCloneable(và IShallowCloneable) giao diện với DeepClone()(và ShallowClone()) phương pháp.

Bạn có thể xác định hai giao diện, một giao diện có tham số chung để hỗ trợ nhân bản được gõ mạnh và một không có khả năng nhân bản được gõ yếu khi bạn làm việc với các bộ sưu tập các loại đối tượng có thể nhân bản khác nhau:

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

Mà sau đó bạn sẽ thực hiện như thế này:

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

Nói chung, tôi thích sử dụng các giao diện được mô tả trái ngược với một hàm tạo sao chép, nó giữ cho ý định rất rõ ràng. Một constructor sao chép có thể được coi là một bản sao sâu sắc, nhưng chắc chắn nó không có nhiều ý định rõ ràng như sử dụng giao diện IDeepClonable.

Điều này được thảo luận trong Nguyên tắc thiết kế khung .net và trên blog của Brad Abrams

(Tôi cho rằng nếu bạn đang viết một ứng dụng (trái ngược với khung / thư viện) để bạn có thể chắc chắn rằng không ai trong nhóm của bạn sẽ gọi mã của bạn, điều đó không quan trọng lắm và bạn có thể gán ý nghĩa ngữ nghĩa của "deepclone" với giao diện IClonizable .net, nhưng bạn nên đảm bảo rằng điều này được ghi lại rõ ràng và được hiểu rõ trong nhóm của bạn. Cá nhân tôi tuân thủ các nguyên tắc khung.)


2
Nếu bạn đang sử dụng giao diện, làm thế nào để có DeepClone (của T) () và DeepClone (của T) (giả như T), cả hai đều trả về T? Cú pháp sau sẽ cho phép T được suy ra dựa trên đối số.
supercat

@supercat: Bạn đang nói có một tham số giả để loại có thể được suy ra? Tôi cho rằng đó là một lựa chọn. Tôi không chắc chắn tôi thích có một tham số giả chỉ để loại tự động suy ra. Có lẽ tôi đang hiểu lầm bạn. (Có thể đăng một số mã trong một câu trả lời mới để tôi có thể thấy ý của bạn).
Simon P Stevens

@supercat: Tham số giả sẽ tồn tại chính xác để cho phép suy luận kiểu. Có những tình huống mà một số mã có thể muốn sao chép một cái gì đó mà không sẵn sàng truy cập vào loại đó là gì (ví dụ vì đó là trường, thuộc tính hoặc hàm trả về từ một lớp khác) và một tham số giả sẽ cho phép một phương tiện suy luận đúng loại. Nghĩ về nó, có lẽ nó không thực sự hữu ích vì toàn bộ điểm của giao diện sẽ là tạo ra thứ gì đó giống như một bộ sưu tập có thể sao chép sâu, trong trường hợp đó, loại nên là loại chung của bộ sưu tập.
supercat

2
Câu hỏi! Trong tình huống nào bạn sẽ muốn phiên bản không chung chung? Đối với tôi, thật có ý nghĩa khi chỉ IDeepCloneable<T>tồn tại, bởi vì ... bạn biết điều gì nếu bạn tự thực hiện, tức làSomeClass : IDeepCloneable<SomeClass> { ... }
Kyle Baran

2
@Kyle nói rằng bạn đã có một phương pháp lấy các đối tượng có thể nhân bản MyFunc(IDeepClonable data), sau đó nó có thể hoạt động trên tất cả các bản sao, không chỉ là một loại cụ thể. Hoặc nếu bạn đã có một bộ sưu tập các bản sao. IEnumerable<IDeepClonable> lotsOfCloneablessau đó bạn có thể sao chép nhiều đối tượng cùng một lúc. Nếu bạn không cần loại đó mặc dù vậy, hãy loại bỏ cái không chung chung.
Simon P Stevens

33

Trong C #, cách ưa thích để thêm chức năng sao chép (sâu) vào một lớp là gì? Có nên thực hiện hàm tạo sao chép, hay xuất phát từ IClonizable và thực hiện phương thức Clone ()?

Vấn đề với ICloneable, như những người khác đã đề cập, nó không chỉ rõ đó là bản sao sâu hay nông, khiến nó thực tế không thể sử dụng được và trong thực tế, hiếm khi được sử dụng. Nó cũng trở lại object, đó là một nỗi đau, vì nó đòi hỏi rất nhiều vai. (Và mặc dù bạn đã đề cập cụ thể đến các lớp trong câu hỏi, việc thực hiện ICloneablemột structmôn quyền anh đòi hỏi.)

Một người lập bản sao cũng bị một trong những vấn đề với IClonizable. Không rõ liệu một nhà xây dựng bản sao đang làm một bản sao sâu hay nông.

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

Tốt nhất nên tạo phương thức DeepClone (). Bằng cách này, ý định là hoàn toàn rõ ràng.

Điều này đặt ra câu hỏi liệu nó nên là một phương thức tĩnh hay cá thể.

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

hoặc là

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

Đôi khi tôi hơi thích phiên bản tĩnh, chỉ vì nhân bản có vẻ như là một cái gì đó đang được thực hiện cho một đối tượng hơn là một cái gì đó mà đối tượng đang làm. Trong cả hai trường hợp, sẽ có những vấn đề cần giải quyết khi nhân bản các đối tượng là một phần của hệ thống phân cấp kế thừa và làm thế nào những vấn đề đó được đưa ra cuối cùng có thể thúc đẩy thiết kế.

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
Mặc dù tôi không biết tùy chọn DeepClone () là tốt nhất, tôi thích câu trả lời của bạn rất nhiều, vì nó nhấn mạnh đến tình huống khó hiểu tồn tại về một tính năng ngôn ngữ lập trình cơ bản theo quan điểm của tôi. Tôi đoán người dùng sẽ chọn tùy chọn nào mình thích nhất.
Dimitri C.

11
Tôi sẽ không tranh luận về các điểm ở đây, nhưng theo tôi, người gọi không nên quan tâm quá nhiều đến sâu hay nông khi họ gọi Clone (). Họ nên biết rằng họ đang nhận được một bản sao không có trạng thái chia sẻ không hợp lệ. Ví dụ, hoàn toàn có thể là trong một bản sao sâu, tôi có thể không muốn sao chép sâu mọi yếu tố. Tất cả những gì người gọi của Clone nên quan tâm là họ đang nhận được một bản sao mới không có bất kỳ tài liệu tham khảo không hợp lệ và không được hỗ trợ nào cho bản gốc. Gọi phương thức 'DeepClone "dường như truyền đạt quá nhiều chi tiết triển khai cho người gọi.
zumalifeguard

1
Có gì sai với một cá thể đối tượng biết cách sao chép chính nó thay vì được sao chép bằng một phương thức tĩnh? Điều này xảy ra trong thế giới thực mọi lúc với các tế bào sinh học. Các tế bào trong cơ thể của bạn đang bận rộn nhân bản ngay bây giờ khi bạn đọc điều này. IMO, tùy chọn phương thức tĩnh cồng kềnh hơn, có xu hướng ẩn chức năng và không sử dụng triển khai "ít gây ngạc nhiên nhất" vì lợi ích của người khác.
Ken Beckett

8
@KenBeckett - Lý do tôi cảm thấy rằng nhân bản là một cái gì đó được thực hiện cho một đối tượng là bởi vì một đối tượng nên "làm một việc và làm tốt". Thông thường, tạo các bản sao của chính nó không phải là năng lực cốt lõi của một lớp, mà đúng hơn đó là chức năng được giải quyết. Tạo một bản sao của BankAccount là điều mà bạn rất có thể muốn làm, nhưng việc tạo bản sao của chính nó không phải là một tính năng của tài khoản ngân hàng. Ví dụ về tế bào của bạn không mang tính hướng dẫn rộng rãi, bởi vì sinh sản chính xác là những gì các tế bào tiến hóa để làm. Cell.Clone sẽ là một phương thức tốt, nhưng điều này không đúng với hầu hết những thứ khác.
Jeffrey L Whitledge

23

Tôi khuyên bạn nên sử dụng một hàm tạo sao chép trên một phương thức sao chép chủ yếu vì một phương thức sao chép sẽ ngăn bạn tạo các trường readonlycó thể có nếu bạn đã sử dụng một hàm tạo thay thế.

Nếu bạn yêu cầu nhân bản đa hình, thì bạn có thể thêm một abstracthoặc virtual Clone()phương thức vào lớp cơ sở mà bạn thực hiện bằng một cuộc gọi đến hàm tạo sao chép.

Nếu bạn yêu cầu nhiều hơn một loại bản sao (ví dụ: sâu / nông), bạn có thể chỉ định nó với một tham số trong hàm tạo sao chép, mặc dù theo kinh nghiệm của tôi, tôi thấy rằng thường là một hỗn hợp sao chép sâu và nông là những gì tôi cần.

Ví dụ:

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 Để giải quyết vấn đề nhân bản đa hình; một ứng dụng quan trọng của nhân bản.
samis

18

Có một lập luận tuyệt vời rằng bạn nên triển khai clone () bằng cách sử dụng hàm tạo sao chép được bảo vệ

Tốt hơn là cung cấp một hàm tạo sao chép được bảo vệ (không công khai) và gọi nó từ phương thức sao chép. Điều này cho chúng ta khả năng ủy thác nhiệm vụ tạo một đối tượng cho một thể hiện của chính lớp đó, do đó cung cấp khả năng mở rộng và cũng có thể tạo các đối tượng một cách an toàn bằng cách sử dụng hàm tạo sao chép được bảo vệ.

Vì vậy, đây không phải là một câu hỏi "so với". Bạn có thể cần cả hai hàm tạo sao chép và giao diện sao chép để thực hiện đúng.

(Mặc dù giao diện công cộng được đề xuất là giao diện Clone () thay vì dựa trên Trình xây dựng.)

Đừng để bị cuốn vào cuộc tranh luận sâu hoặc nông rõ ràng trong các câu trả lời khác. Trong thế giới thực, hầu như luôn luôn có một cái gì đó ở giữa - và dù bằng cách nào, không nên là mối quan tâm của người gọi.

Hợp đồng Clone () chỉ đơn giản là "sẽ không thay đổi khi tôi thay đổi hợp đồng đầu tiên". Bao nhiêu biểu đồ bạn phải sao chép hoặc cách bạn tránh đệ quy vô hạn để thực hiện điều đó không nên quan tâm đến người gọi.


"Không nên là mối quan tâm của người gọi". Tôi không thể đồng ý nhiều hơn, nhưng tôi ở đây, cố gắng tìm hiểu xem Danh sách <T> aList = Danh sách mới <T> (aFullListOfT) sẽ thực hiện một bản sao sâu (đó là những gì tôi muốn) hoặc Bản sao nông (sẽ phá vỡ mã của tôi) và liệu tôi có phải thực hiện một cách khác để hoàn thành công việc hay không!
ThunderGr

3
Một danh sách <T> quá chung chung (ha ha) để nhân bản thậm chí có ý nghĩa. Trong trường hợp của bạn, đó chắc chắn chỉ là một bản sao của danh sách và KHÔNG phải là các đối tượng được chỉ ra bởi danh sách. Thao tác với danh sách mới sẽ không ảnh hưởng đến danh sách đầu tiên, nhưng các đối tượng là như nhau và trừ khi chúng không thay đổi, các đối tượng trong tập đầu tiên sẽ thay đổi nếu bạn thay đổi danh sách trong tập thứ hai. Nếu có một hoạt động list.Clone () trong thư viện của bạn, bạn nên mong đợi kết quả sẽ là một bản sao đầy đủ như trong "sẽ không thay đổi khi tôi làm điều gì đó với cái đầu tiên." cũng áp dụng cho các đối tượng chứa.
DanO

1
Danh sách <T> sẽ không biết gì thêm về việc sao chép chính xác nội dung của nó so với bạn. Nếu đối tượng cơ bản là bất biến, bạn tốt để đi. Mặt khác, nếu đối tượng cơ bản có phương thức Clone (), bạn sẽ phải sử dụng nó. Danh sách <T> aList = Danh sách mới <T> (aFullListOfT.Select (t = t.Clone ())
DanO

1
+1 cho phương pháp lai. Cả hai phương pháp đều có một ưu điểm và nhược điểm, nhưng điều này dường như có lợi ích tổng thể hơn.
Kyle Baran

12

Việc triển khai IClonizable không được khuyến nghị do thực tế là nó không được chỉ định cho dù đó là bản sao sâu hay nông, vì vậy tôi sẽ tìm đến nhà xây dựng hoặc chỉ tự mình thực hiện một cái gì đó. Có thể gọi nó là DeepCopy () để làm cho nó thực sự rõ ràng!


5
@Grant, làm thế nào để xây dựng ý định chuyển tiếp? IOW, nếu một đối tượng tự lấy trong hàm tạo, bản sao sâu hay nông? Mặt khác, tôi hoàn toàn đồng ý với đề xuất DeepCopy () (hoặc nếu không).
Marc

7
Tôi cho rằng một hàm tạo gần như không rõ ràng như giao diện IClonizable - bạn phải đọc các tài liệu / mã API để biết nó có sao chép sâu hay không. Tôi chỉ định nghĩa một IDeepCloneable<T>giao diện với một DeepClone()phương thức.
Kent Boogaart

2
@Jon - Lò phản ứng chưa bao giờ kết thúc!
Grant Crofton

@Marc, @Kent - vâng công bằng, nhà xây dựng có lẽ cũng không phải là một ý tưởng tốt.
Grant Crofton

3
Có ai nhìn thấy việc sử dụng iClonizable được sử dụng trên một đối tượng không xác định? Điểm chung của các giao diện là chúng có thể được sử dụng trên các đối tượng không xác định; mặt khác, người ta có thể đơn giản biến Clone thành một phương thức chuẩn trả về kiểu đang được đề cập.
supercat

12

Bạn sẽ gặp vấn đề với các nhà xây dựng sao chép và các lớp trừu tượng. Hãy tưởng tượng bạn muốn làm như sau:

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

Ngay sau khi thực hiện các thao tác trên, bạn bắt đầu muốn sử dụng giao diện hoặc giải quyết triển khai DeepCopy () / IClonizable.Clone ().


2
Đối số tốt cho một cách tiếp cận dựa trên giao diện.
DanO

4

Vấn đề với IClonizable là cả ý định và tính nhất quán. Nó không bao giờ rõ ràng cho dù đó là một bản sao sâu hay nông. Do đó, có lẽ nó không bao giờ được sử dụng theo cách này hay cách khác.

Tôi không tìm thấy một nhà xây dựng bản sao công khai để rõ ràng hơn về vấn đề đó.

Điều đó nói rằng, tôi sẽ giới thiệu một hệ thống phương pháp phù hợp với bạn và chuyển tiếp ý định (a'la hơi tự ghi lại tài liệu)


3

Nếu đối tượng bạn đang cố sao chép là Tuần tự hóa, bạn có thể sao chép nó bằng cách tuần tự hóa nó và giải tuần tự hóa nó. Sau đó, bạn không cần phải viết một hàm tạo sao chép cho mỗi lớp.

Tôi không có quyền truy cập vào mã ngay bây giờ nhưng nó là một cái gì đó như thế này

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
Việc sử dụng tuần tự hóa như một phương tiện để thực hiện nhân bản sâu không liên quan đến câu hỏi liệu bản sao sâu nên được nổi lên như một ctor hay phương pháp.
Kent Boogaart

1
Tôi nghĩ đó là một sự thay thế hợp lệ. Tôi không nghĩ anh ta bị giới hạn trong hai phương pháp sao chép sâu.
Shaun Bowe

5
@Kent Boogaart - Với OP bắt đầu bằng dòng "Trong C #, cách ưu tiên để thêm chức năng sao chép (sâu) vào một lớp", tôi nghĩ rằng Shaun đủ công bằng để đưa ra các lựa chọn thay thế khác. Đặc biệt trong một kịch bản kế thừa nơi bạn có một số lượng lớn các lớp mà bạn muốn thực hiện chức năng nhân bản cho, thủ thuật này có thể hữu ích; không nhẹ như thực hiện bản sao của bạn trực tiếp, nhưng vẫn hữu ích. Nếu mọi người không bao giờ đưa ra "bạn có nghĩ đến ..." thay thế cho câu hỏi của tôi không, thì tôi đã không học được nhiều như tôi trong nhiều năm qua.
Rob Levine

2

Nó phụ thuộc vào ngữ nghĩa sao chép của lớp đang đề cập, mà bạn nên xác định mình là nhà phát triển. Phương pháp chọn thường dựa trên các trường hợp sử dụng dự định của lớp. Có lẽ nó sẽ có ý nghĩa để thực hiện cả hai phương pháp. Nhưng cả hai đều có chung nhược điểm - không rõ chính xác phương pháp sao chép nào họ thực hiện. Điều này cần được nêu rõ trong tài liệu cho lớp học của bạn.

Đối với tôi có:

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

thay vì:

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

nhìn như tuyên bố rõ ràng hơn về ý định.


0

Tôi nghĩ rằng nên có một mẫu chuẩn cho các đối tượng có thể nhân bản, mặc dù tôi không chắc chính xác mẫu đó phải là gì. Liên quan đến nhân bản, dường như có ba loại lớp:

  1. Những người rõ ràng hỗ trợ cho nhân bản sâu
  2. Những nơi mà nhân bản thành viên sẽ hoạt động như nhân bản sâu, nhưng không có hoặc không cần hỗ trợ rõ ràng.
  3. Những người không thể được nhân bản sâu một cách hữu ích và khi nhân bản thành viên sẽ mang lại kết quả xấu.

Theo như tôi có thể nói, cách duy nhất (ít nhất là trong .net 2.0) để có được một đối tượng mới cùng loại với một đối tượng hiện có là sử dụng MemberwiseClone. Một mô hình đẹp dường như có chức năng "mới" / "Bóng tối" luôn luôn trả về kiểu hiện tại, với định nghĩa luôn là gọi MemberwiseClone và sau đó gọi chương trình con ảo được bảo vệ CleanupClone (gốcObject). Thói quen CleanupCode nên gọi base.Cleanupcode để xử lý các nhu cầu nhân bản của loại cơ sở và sau đó thêm dọn dẹp riêng. Nếu thói quen nhân bản phải sử dụng đối tượng ban đầu, thì nó sẽ phải là typecast, nhưng nếu không thì kiểu truyền hình duy nhất sẽ có trong lệnh gọi MemberwiseClone.

Thật không may, cấp độ thấp nhất của loại (1) ở trên thay vì loại (2) sẽ phải được mã hóa để cho rằng các loại thấp hơn sẽ không cần bất kỳ sự hỗ trợ rõ ràng nào để nhân bản. Tôi không thực sự thấy bất kỳ cách nào xung quanh đó.

Tuy nhiên, tôi nghĩ rằng có một mô hình xác định sẽ tốt hơn không có gì.

Ngẫu nhiên, nếu ai đó biết rằng loại cơ sở của một người hỗ trợ iClonizable, nhưng không biết tên của chức năng mà nó sử dụng, có cách nào để tham chiếu chức năng iClonizable.Clone của loại cơ sở của một người không?


0

Nếu bạn đọc qua tất cả các câu trả lời và thảo luận thú vị, bạn vẫn có thể tự hỏi làm thế nào bạn sao chép chính xác các thuộc tính - tất cả chúng đều rõ ràng, hoặc có cách nào thanh lịch hơn để làm điều đó? Nếu đó là câu hỏi còn lại của bạn, hãy xem điều này (tại StackOverflow):

Làm thế nào để tôi có thể sao chép sâu các bản sao của các lớp bên thứ 3 bằng cách sử dụng một phương thức mở rộng chung?

Nó mô tả cách thực hiện một phương thức mở rộng CreateCopy()tạo ra một bản sao "sâu" của đối tượng bao gồm tất cả các thuộc tính (mà không phải sao chép thuộc tính theo cách thủ công).

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.