复制构造函数与Clone()


Answers:


91

您不应该源自ICloneable

原因是,当Microsoft设计.net框架时,他们从未指定Clone()方法ICloneable是深克隆还是浅克隆,因此接口在语义上被破坏,因为调用者将不知道该调用将深克隆还是浅克隆对象。

相反,您应该使用(和)方法定义自己的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设计指南Brad Abrams的博客中对此进行了讨论。

(我想如果您是在编写应用程序(而不是框架/库),那么可以确保团队之外的任何人都不会调用您的代码,这无关紧要,您可以分配“深层克隆”到.net ICloneable接口,但是您应该确保在团队中对此进行了充分的文档记录和理解。就我个人而言,我会遵循框架准则。)


2
如果要使用接口,将DeepClone(of T)()和DeepClone(of T)(dummy as T)都返回T怎么办?后一种语法将允许根据参数推断T。
超级猫

@supercat:您是说有一个虚拟参数,以便可以推断出类型吗?我想这是一个选择。我不确定我是否喜欢使用虚拟参数只是为了自动推断类型。也许我误会了你。(也许在新答案中发布一些代码,以便我明白您的意思)。
西蒙·史蒂文斯

@supercat:伪参数将恰好存在以允许类型推断。在某些情况下,某些代码可能想要克隆某些东西而无法立即访问类型(例如,因为它是来自另一个类的字段,属性或函数),而虚拟参数将允许适当地推断类型。考虑一下,它可能并没有真正的用处,因为接口的全部目的是创建类似可深克隆的集合之类的东西,在这种情况下,类型应该是集合的泛型类型。
超级猫

2
题!在什么情况下,您会需要非通用版本?对我来说,仅IDeepCloneable<T>存在是有意义的,因为...如果您自己进行实现,您确实知道T,即SomeClass : IDeepCloneable<SomeClass> { ... }
Kyle Baran 2014年

2
@Kyle说您有一个采用MyFunc(IDeepClonable data)可克隆对象的方法,那么它可以在所有可克隆对象上工作,而不仅限于特定类型。或者,如果您有大量的可克隆对象。IEnumerable<IDeepClonable> lotsOfCloneables那么您可以同时克隆很多对象。如果您不需要那种东西,那么将非泛型的东西排除在外。
Simon P Stevens 2014年

33

在C#中,向类添加(深度)复制功能的首选方法是什么?应该实现复制构造函数,还是应该从IClo​​neable派生并实现Clone()方法?

ICloneable正如其他人提到的那样,它的问题在于它没有指定是深拷贝还是浅拷贝,这使得它实际上不可用,并且在实践中很少使用。它也返回object,这很痛苦,因为它需要大量的转换。(尽管您在问题中特别提到了类,但ICloneablestructrequire装箱中实现。)

复制构造者还遭受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
知道如何克隆自己而不是通过静态方法复制的对象实例怎么了?在生物细胞中,这一直发生在现实世界中。阅读本文时,您自己体内的细胞正忙于克隆自己。IMO,静态方法选项较为繁琐,倾向于隐藏功能,并且为了他人的利益而偏离使用“最令人惊讶”的实现。
肯·贝克特

8
@KenBeckett-我之所以认为克隆是对对象执行的操作,是因为对象应该“做一件事情并且做好事”。通常,复制自身不是类的核心能力,而是所附加的功能。克隆BankAccount是您很想做的事情,但是克隆自身并不是银行帐户的功能。您的细胞示例没有广泛的指导意义,因为繁殖正是细胞进化而成的。Cell.Clone将是一个很好的实例方法,但是对于大多数其他情况而言并非如此。
Jeffrey L Whitledge,2012年

23

我建议在复制方法上使用复制构造函数,主要是因为复制方法会阻止您创建字段 readonly原本使用构造函数的。

如果需要多态克隆,则可以通过调用复制构造函数在实现的基类中添加abstractvirtual Clone()方法。

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

例如:

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用于解决多态克隆;克隆的重要应用。
samis 2012年

18

有一个很好的论点,您应该使用受保护的副本构造函数实现clone()

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

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

(尽管推荐的公共接口是Clone()接口,而不是基于Constructor的接口。)

不要陷入其他答案中明确的深层或浅层争论。在现实世界中,这几乎总是介于两者之间-无论哪种方式,都不应该是呼叫者关心的问题。

Clone()合约只是“当我更改第一个合约时不会更改”。您必须复制多少图,或者如何避免无限递归来实现这一点,与调用者无关。


“不应成为呼叫者的问题”。我完全不同意,但是在这里,我想弄清楚List <T> aList = new List <T>(aFullListOfT)是做深层复制(这就是我想要的)还是浅层复制(会破坏)我的代码)以及是否必须实现另一种方式来完成工作!
ThunderGr

3
list <T>太笼统了(哈哈),无法克隆。在您的情况下,那肯定只是列表的副本,而不是列表所指向的对象。操纵新列表不会影响第一个列表,但是对象是相同的,除非它们是不可变的,否则如果您更改第二个集合中的对象,则第一个集合中的对象将更改。如果您的库中有一个list.Clone()操作,您应该期望结果是完整的克隆,如“当我对第一个操作执行某操作时不会更改”中所示。也适用于所包含的对象。
DanO 2014年

1
List <T>不会比您更了解有关正确克隆其内容的任何信息。如果基础对象是不可变的,那您就走了。否则,如果基础对象具有Clone()方法,则必须使用它。List <T> aList = new List <T>(aFullListOfT.Select(t = t.Clone())
DanO 2014年

1
+1为混合方式。两种方法都有其优点和缺点,但这似乎具有更大的整体好处。
凯尔·巴兰

12

不建议实施ICloneable,因为没有指定它是深拷贝还是浅拷贝,因此我会选择构造函数,或者自己实现一些东西。也许将其称为DeepCopy()使其变得明显!


5
@Grant,构造函数如何传递意图?IOW,如果一个对象进入了构造函数,那么副本是深层还是浅层?否则,我完全同意DeepCopy()(或其他方式)的建议。
马克

7
我认为构造函数几乎和ICloneable接口一样不清楚-您必须阅读API文档/代码才能知道它是否进行了深度克隆。我只是IDeepCloneable<T>DeepClone()方法定义接口。
肯特·布加亚特

2
@Jon-反应堆从未完成!
Grant Crofton 2010年

@Marc,@Kent-是的,构建器也不是一个好主意。
Grant Crofton 2010年

3
有没有人看到过在未知类型的对象上使用iCloneable的用法?接口的全部意义在于它们可以用于未知类型的对象。否则,也可以简单地使Clone成为返回所讨论类型的标准方法。
超级猫

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 2013年

4

ICloneable的问题在于意图和一致性。不清楚是深拷贝还是浅拷贝。因此,它可能永远不会以一种或另一种方式使用。

在这件事上,我找不到公共副本构造函数。

就是说,我将介绍一个对您有用的方法系统并传达意图(某种程度上是自我记录的)


3

如果您要复制的对象是可序列化的,则可以通过序列化和反序列化来克隆它。然后,您无需为每个类编写副本构造函数。

我现在无法访问该代码,但这是这样的

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
使用序列化作为实现深度克隆的手段与深度克隆是否应作为ctor或方法浮出水面的问题无关。
肯特·布加亚特

1
我认为这是另一种有效的选择。我认为他并不局限于这两种深度复制方法。
肖恩·鲍伊

5
@Kent Boogaart-鉴于OP以“在C#中,向类中添加(深度)复制功能的首选方式”这一行开头,我认为Shaun提供不同的选择就足够了。尤其是在遗留场景中,您要为它实现克隆功能的类很多,此技巧很有用;虽然不像直接实现自己的克隆那样轻巧,但是仍然很有用。如果人们从未对我的问题提供“您想过...”的替代方案,那么我过去几年所学到的知识几乎不多。
罗布·莱文

2

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

对我而言:

// 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

我认为应该为可克隆对象提供一个标准模式,尽管我不确定该模式到底应该是什么。关于克隆,似乎有三种类型的类:

  1. 那些明确支持深度克隆的软件
  2. 那些按成员克隆将作为深度克隆工作,但既不需要也不需要明确支持的对象。
  3. 那些不能进行有用的深度克隆的方法,以及进行逐成员克隆将产生不良结果的方法。

据我所知,(至少在.net 2.0中)获取与现有对象相同类的新对象的唯一方法是使用MemberwiseClone。一个不错的模式似乎是具有“ new” /“ Shadows”函数Clone,该函数始终返回当前类型,其定义始终是调用MemberwiseClone,然后调用受保护的虚拟子例程CleanupClone(originalObject)。CleanupCode例程应调用base.Cleanupcode来处理基本类型的克隆需求,然后添加其自己的清理。如果克隆例程必须使用原始对象,则必须进行类型转换,否则,唯一的类型转换将在MemberwiseClone调用上进行。

不幸的是,必须对上面类型(1)而不是类型(2)的最低级别进行编码,以假定其较低类型不需要任何显式支持克隆。我真的没有办法解决这个问题。

不过,我认为拥有定义的模式总比没有好。

顺便说一句,如果一个人知道自己的基本类型支持iCloneable,但不知道其使用的函数的名称,是否有任何方法可以引用一个人的基本类型的iCloneable.Clone函数?


0

如果您通读了所有有趣的答案和讨论,您仍可能会问自己,您如何精确地复制属性-明确地复制所有属性,还是有一种更优雅的方法?如果这是您剩下的问题,请查看以下内容(在StackOverflow上):

如何使用通用扩展方法“深入”克隆第三方类的属性?

它描述了如何实现扩展方法CreateCopy(),该方法将创建对象的“深层”副本,包括所有属性(而不必手动逐个复制属性)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.