复制构造函数与Clone()方法

128
12个回答

100

你不应该从ICloneable派生。

原因是当微软设计.NET框架时,他们从未指定ICloneable上的Clone()方法应该是深克隆还是浅克隆,因此接口在语义上是有问题的,因为调用者不知道调用是否会深度或浅度克隆对象。

相反,你应该定义自己的IDeepCloneable(和IShallowCloneable)接口,并使用DeepClone()(和ShallowClone())方法。

你可以定义两个接口,一个带有泛型参数以支持强类型克隆,另一个没有泛型参数以保留弱类型克隆能力,当你处理不同类型的可克隆对象集合时使用:

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

然后您可以像这样实现:

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

通常,我更喜欢使用接口而不是副本构造函数来描述代码,因为这样可以使意图非常明确。副本构造函数可能会被认为是一个深层克隆,但与使用IDeepClonable接口相比,它的意图肯定不够清晰。
在.NET Framework Design GuidelinesBrad Abrams'博客中都有讨论此话题。
(我想如果您正在编写应用程序(而不是框架/库),那么您可以确定您的团队之外没有人会调用您的代码,那么这可能并不重要,并且您可以将"deepclone"赋予.NET ICloneable接口语义含义,但是您应该确保这在您的团队内得到了很好的记录和理解。个人而言,我会遵循框架指南。)

2
如果你要使用接口,那么考虑一下是否可以有DeepClone(of T)()和DeepClone(of T)(dummy as T)两个方法,它们都返回T类型?后者的语法将允许根据参数推断出T的类型。 - supercat
@supercat:虚拟参数存在的目的正是为了允许类型推断。有些情况下,一些代码可能希望克隆某个对象,但没有准备好访问该类型(例如,因为它是另一个类的字段、属性或函数返回值),虚拟参数将允许正确推断类型。仔细思考后,这可能并不真正有帮助,因为接口的整个目的就是创建像可深度克隆集合这样的东西,在这种情况下,类型应该是集合的泛型类型。 - supercat
2
问题!在什么情况下您会想要非泛型版本?对我来说,只有 IDeepCloneable<T> 存在是有意义的,因为...如果您制作自己的实现,您知道 T 是什么,例如 SomeClass : IDeepCloneable<SomeClass> { ... } - Kyle Baran
2
@Kyle说,如果你有一个接受可克隆对象的方法 MyFunc(IDeepClonable data),那么它可以处理所有可克隆对象,而不仅仅是特定类型的对象。或者,如果你有一组可克隆对象 IEnumerable<IDeepClonable> lotsOfCloneables,那么你可以同时克隆许多对象。但如果你不需要这种功能,那么就不要使用非泛型的方法。 - Simon P Stevens
那么方法参数呢?如果我没有实现任何接口,它们会如何被复制? - Johnny_D
显示剩余4条评论

33
在C#中,如何向类添加(深层)复制功能是什么?应该实现复制构造函数,还是继承ICloneable并实现Clone()方法?
ICloneable的问题在于,正如其他人所提到的那样,它没有指定是深拷贝还是浅拷贝,这使得它在实践中几乎无用且很少使用。它也返回对象,这很麻烦,因为它需要大量的强制类型转换。(尽管你在问题中特别提到了类,但在结构(struct)上实现ICloneable需要装箱(boxing))
复制构造函数也存在与ICloneable相同的问题。不清楚复制构造函数是否正在执行深层或浅层复制。
Account clonedAccount = new Account(currentAccount); // Deep or shallow?

最好创建一个DeepClone()方法。这样意图就非常清晰。

这引出了一个问题,它应该是静态方法还是实例方法。

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

有时我稍微更喜欢使用静态版本,只是因为克隆似乎是针对一个对象进行的操作,而不是对象本身在执行的操作。无论哪种情况,当克隆属于继承层次结构的对象时,都需要处理问题,如何处理这些问题可能最终会影响设计。

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
虽然我不知道DeepClone()选项是否最佳选择,但我非常喜欢你的回答,因为它强调了一个在我看来非常基础的编程语言特性中存在的混淆情况。我猜这取决于用户选择哪个选项更好。 - Dimitri C.
11
我不会在这里辩论这些观点,但我认为,当调用Clone()时,调用者不应太关心是深度克隆还是浅克隆。他们只需要知道他们得到了一个克隆版本,该版本没有无效的共享状态。例如,在深度克隆中,可能并不想深度克隆每个元素。对于Clone的调用者来说,所有需要关心的就是他们得到了一个新的副本,没有任何对原始对象的无效和不支持的引用。将方法命名为“DeepClone”似乎向调用方传达了太多的实现细节。 - zumalifeguard
1
一个对象实例知道如何克隆自己而不是被静态方法复制,这有什么问题吗?在现实世界中,生物细胞经常这样做。当您阅读此文时,您身体中的细胞正在忙于克隆自己。在我看来,静态方法选项更加繁琐,往往隐藏了功能,并偏离了使用“最少惊讶”实现以造福他人的原则。 - Ken Beckett
8
我认为克隆是对一个对象进行操作的原因是因为一个对象应该“做一件事并且做得很好”。通常,复制自身并不是一个类的核心能力,而是附加功能。制作银行账户的克隆是您可能想要做的事情,但是复制自身并不是银行账户的特点。细胞的例子并不具有普遍指导意义,因为繁殖正是细胞进化的目的。Cell.Clone可能是一个好的实例方法,但对大多数其他事物来说并非如此。 - Jeffrey L Whitledge

29

我建议使用复制构造函数而不是克隆方法,主要是因为如果您使用构造函数,可以将某些字段设置为readonly,而这在使用克隆方法时是无法做到的。

如果您需要多态克隆,则可以在基类中添加一个abstractvirtualClone()方法,并在其中调用复制构造函数。

如果您需要多种类型的复制(例如:深复制/浅复制),则可以在复制构造函数中使用参数来指定它们。但根据我的经验,通常需要深度和浅度复制的混合。

例如:

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

11
+1 是为了解决多态克隆而提出的,这是克隆的一个重要应用。 - samus

19

有一个很好的论点,即您应该使用受保护的复制构造函数implement clone() using a protected copy constructor

最好提供一个受保护(非公开)的复制构造函数,并从克隆方法调用它。这使我们能够将创建对象的任务委托给类本身的实例,从而提供可扩展性,并使用受保护的复制构造函数安全地创建对象。

因此,这不是一个“对立”的问题。您可能需要使用复制构造函数和克隆接口来正确执行。

(尽管推荐的公共接口是基于克隆的接口而不是基于构造函数的接口。)

不要陷入其他答案中的明确深层或浅层参数的争论中。在现实世界中,它几乎总是介于两者之间 - 无论如何,都不应该是调用者关心的问题。

Clone()合同只是“当我更改第一个时不会更改”。您必须复制多少图形,或者如何避免无限递归以使其发生,都不应该让调用者担心。


“不应该是调用者的问题”。我非常同意,但是在这里,我正在尝试弄清楚List<T> aList = new List<T>(aFullListOfT)是否会进行深拷贝(这是我想要的),还是浅拷贝(这将破坏我的代码),以及我是否需要实现另一种方法来完成工作! - ThunderGr
3
一个list<T>太过于泛化(哈哈),克隆甚至不具备意义。 在你的情况下,这仅仅是列表的副本,而不是指向该列表对象的副本。 操作这个新列表不会影响第一个列表,但是对象是相同的,除非它们是不可变的,否则第一组中的对象将在更改第二组中的对象时发生更改。如果你的库中有一个list.Clone()操作,你应该期望结果是完全克隆,就像“当我对第一个进行某些操作时不会改变”。适用于包含的对象也是如此。 - DanO
1
一个 List<T> 并不会比你更了解如何正确地克隆它的内容。如果底层对象是不可变的,那么你就可以放心使用它了。否则,如果底层对象有一个 Clone() 方法,你就必须使用它。List<T> aList = new List<T>(aFullListOfT.Select(t=t.Clone()) - DanO
1
+1 为混合方法。两种方法都有优点和缺点,但这种方法似乎具有更多的整体好处。 - Kyle Baran

12
你将会遇到复制构造函数和抽象类的问题。想象一下你想要做以下操作:
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));
        }
    }
}

在执行上述操作后,您会开始希望要么使用接口,要么使用DeepCopy()/ICloneable.Clone()实现。


2
面向接口编程方法的优势。 - DanO

12

不建议实现ICloneable接口,因为它没有指定是深拷贝还是浅拷贝,所以我建议使用构造函数或自己实现一个方法。也许可以将其命名为DeepCopy()以使其更加明显!


5
@Grant,构造函数如何传递意图?换句话说,如果一个对象在构造函数中复制自身,是深拷贝还是浅拷贝?除此之外,我完全同意DeepCopy()(或其他方式)的建议。 - Marc
7
我认为构造函数和ICloneable接口一样不太清晰 - 你得阅读API文档/代码才能知道它是否进行深拷贝。我只是定义了一个带有DeepClone()方法的IDeepCloneable<T>接口来实现深度克隆。 - Kent Boogaart
2
@Jon - 重构永远不会完成! - Grant Crofton
糟糕,我发现我们的一些代码实现了ICloneable :-( - John Warlow
3
有人看到过在未知类型的对象上使用 iCloneable 接口吗?接口的整个意义在于它们可以用于未知类型的对象;否则,我们可能会将 Clone 设为返回所涉及类型的标准方法。 - supercat
显示剩余3条评论

4
如果您要复制的对象是可序列化的,那么您可以通过对其进行序列化和反序列化来克隆它。这样,您就不需要为每个类编写一个复制构造函数。目前我无法查看代码,但大致如下。
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
使用序列化作为实现深层克隆的手段,与深层克隆是否应该作为构造函数或方法公开无关。 - Kent Boogaart
1
我认为这是另一个有效的选择。我并不认为他被限制在那两种深拷贝方法中。 - Shaun Bowe
6
鉴于原始问题的开头是“在C#中,将(深度)复制功能添加到类的首选方式是什么”,我认为允许Shaun提供不同的选择是公平的。特别是在旧有场景中,如果您想要为大量类实现克隆功能,这种技巧可能很有用;虽然不如直接实现自己的克隆那样轻量级,但仍然有用。如果人们从未对我的问题提供“你是否考虑过......”的替代方案,我学到的知识也不会那么丰富。 - Rob Levine

4

ICloneable的问题在于意图和一致性。它从未清楚地表明它是深拷贝还是浅拷贝。因此,它可能永远不会以完全相同的方式使用。

我认为公共复制构造函数对这个问题也没有更清晰的解释。

话虽如此,我建议引入一个适合您的方法系统并传达意图(类似于某种自我记录)。


1

这取决于所涉及类的复制语义,开发人员应该自己定义。选择的方法通常基于类的预期用例。也许实现两种方法都有意义。但是两种方法都具有相似的缺点-不清楚它们实现了哪种复制方法。这应该在您的类文档中明确说明。

对我来说:

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

改为:

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

看起来更清晰的意图陈述。


0
使用NewtonSoft.Json来完成这个非常简单。
// get a MyClass object 'myObject' to create a clone
public class MyClass {
    public MyClass Copy()
        => Newtonsoft.Json.JsonConvert.DeserializeObject<MyClass >(Newtonsoft.Json.JsonConvert.SerializeObject(this)) ?? new();
}

MyClass myObject = new(MyClass);
MyClass myCopy = myObject.Copy();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接