在C#中,向类添加(深度)复制功能的首选方法是什么?是应该实现副本构造函数,还是应该派生ICloneable并实现该Clone()方法?
备注:我在括号内写了“ deep”,因为我认为这无关紧要。显然其他人不同意,所以我问是否复制构造函数/运算符/函数需要弄清楚它实现的是哪种复制变体。
在C#中,向类添加(深度)复制功能的首选方法是什么?是应该实现副本构造函数,还是应该派生ICloneable并实现该Clone()方法?
备注:我在括号内写了“ deep”,因为我认为这无关紧要。显然其他人不同意,所以我问是否复制构造函数/运算符/函数需要弄清楚它实现的是哪种复制变体。
Answers:
您不应该源自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接口,但是您应该确保在团队中对此进行了充分的文档记录和理解。就我个人而言,我会遵循框架准则。)
IDeepCloneable<T>存在是有意义的,因为...如果您自己进行实现,您确实知道T,即SomeClass : IDeepCloneable<SomeClass> { ... }
MyFunc(IDeepClonable data)可克隆对象的方法,那么它可以在所有可克隆对象上工作,而不仅限于特定类型。或者,如果您有大量的可克隆对象。IEnumerable<IDeepClonable> lotsOfCloneables那么您可以同时克隆很多对象。如果您不需要那种东西,那么将非泛型的东西排除在外。
在C#中,向类添加(深度)复制功能的首选方法是什么?应该实现复制构造函数,还是应该从ICloneable派生并实现Clone()方法?
ICloneable正如其他人提到的那样,它的问题在于它没有指定是深拷贝还是浅拷贝,这使得它实际上不可用,并且在实践中很少使用。它也返回object,这很痛苦,因为它需要大量的转换。(尽管您在问题中特别提到了类,但ICloneable在structrequire装箱中实现。)
复制构造者还遭受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();
}
}
我建议在复制方法上使用复制构造函数,主要是因为复制方法会阻止您创建字段 readonly原本使用构造函数的。
如果需要多态克隆,则可以通过调用复制构造函数在实现的基类中添加abstract或virtual 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);
}
有一个很好的论点,您应该使用受保护的副本构造函数实现clone()
最好提供一个受保护的(非公共)副本构造函数,然后从clone方法中调用它。这使我们能够将创建对象的任务委托给类本身的实例,从而提供可扩展性,并且还可以使用受保护的副本构造函数安全地创建对象。
因此,这不是一个“相对”的问题。您可能需要复制构造函数和克隆接口才能正确执行。
(尽管推荐的公共接口是Clone()接口,而不是基于Constructor的接口。)
不要陷入其他答案中明确的深层或浅层争论。在现实世界中,这几乎总是介于两者之间-无论哪种方式,都不应该是呼叫者关心的问题。
Clone()合约只是“当我更改第一个合约时不会更改”。您必须复制多少图,或者如何避免无限递归来实现这一点,与调用者无关。
不建议实施ICloneable,因为没有指定它是深拷贝还是浅拷贝,因此我会选择构造函数,或者自己实现一些东西。也许将其称为DeepCopy()使其变得明显!
IDeepCloneable<T>用DeepClone()方法定义接口。
您将遇到复制构造函数和抽象类的问题。假设您要执行以下操作:
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()实现。
如果您要复制的对象是可序列化的,则可以通过序列化和反序列化来克隆它。然后,您无需为每个类编写副本构造函数。
我现在无法访问该代码,但这是这样的
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
}
它取决于相关类的复制语义,您应该将自己定义为开发人员。选择方法通常基于类的预期用例。也许实现两种方法都有意义。但是两者都有类似的缺点-目前尚不清楚它们实现哪种复制方法。这应该在您班级的文档中明确说明。
对我而言:
// 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();
看起来是更清晰的意图声明。
我认为应该为可克隆对象提供一个标准模式,尽管我不确定该模式到底应该是什么。关于克隆,似乎有三种类型的类:
据我所知,(至少在.net 2.0中)获取与现有对象相同类的新对象的唯一方法是使用MemberwiseClone。一个不错的模式似乎是具有“ new” /“ Shadows”函数Clone,该函数始终返回当前类型,其定义始终是调用MemberwiseClone,然后调用受保护的虚拟子例程CleanupClone(originalObject)。CleanupCode例程应调用base.Cleanupcode来处理基本类型的克隆需求,然后添加其自己的清理。如果克隆例程必须使用原始对象,则必须进行类型转换,否则,唯一的类型转换将在MemberwiseClone调用上进行。
不幸的是,必须对上面类型(1)而不是类型(2)的最低级别进行编码,以假定其较低类型不需要任何显式支持克隆。我真的没有办法解决这个问题。
不过,我认为拥有定义的模式总比没有好。
顺便说一句,如果一个人知道自己的基本类型支持iCloneable,但不知道其使用的函数的名称,是否有任何方法可以引用一个人的基本类型的iCloneable.Clone函数?
如果您通读了所有有趣的答案和讨论,您仍可能会问自己,您如何精确地复制属性-明确地复制所有属性,还是有一种更优雅的方法?如果这是您剩下的问题,请查看以下内容(在StackOverflow上):
它描述了如何实现扩展方法CreateCopy(),该方法将创建对象的“深层”副本,包括所有属性(而不必手动逐个复制属性)。