在实现ICloneable接口时,我是否应该提供一个深度克隆?

6

MSDN文档中,我并不清楚在实现ICloneable接口时应该提供深克隆还是浅克隆。哪种选项更好呢?

3个回答

15

简短回答:是的。

长篇回答:不要使用 ICloneable 接口。这是因为 .Clone 方法没有定义其是浅克隆还是深克隆。你应该实现自己的 IClone 接口,并描述如何进行克隆操作。


1
鉴于对象的定义方式,不应该有关于“深度克隆”和“浅克隆”的疑问。如果一个对象封装了事物的身份,则该对象的克隆应该封装相同事物的身份。如果一个对象封装了可变对象的值,则其副本应封装持有相同值的分离可变对象。
不幸的是,.NET和Java都没有在类型系统中包括引用是封装标识、可变值、两者还是均非的信息。相反,它们只使用单个引用类型,并认为拥有引用的唯一副本的代码,或拥有容器的唯一引用的代码,可以使用该引用来封装值或状态。这样的思考可能对单个对象是可以容忍的,但涉及到复制和相等性测试操作时会带来真正的问题。
如果一个类有一个字段Foo,它封装了一个List的状态,该列表将封装其中对象的身份,并且可能在将来封装不同对象的身份,则Foo的克隆应该引用标识相同对象的新列表。如果List用于封装对象的可变状态,则克隆应该引用标识具有相同状态的新对象的新列表。
如果对象包含独立的“等效”和“相等”方法,并且每个堆对象类型都有被表示为封装身份、可变状态、两者还是均非的引用类型,则99%的相等性测试和克隆方法可以自动处理。如果所有组件封装标识或可变状态,则两个聚合物相等,如果它们仅相等而不是等效,同时封装了其他内容则至少相等;只有当所有对应的组件都是并且始终将是等效时,两个聚合物才是等效的[这通常意味着引用相等,但并非总是如此]。复制聚合需要制作封装可变状态的每个成分的分离副本,复制封装标识的每个成分的引用,并对那些封装其他内容的成分执行上述操作;同时封装可变状态和标识的成分的聚合不能简单地进行克隆。
这样的克隆、相等性和等效性规则存在一些棘手的情况无法正确处理,但如果有一个约定来区分List和List,并支持“等效”和“相等”测试,则99%的对象可以自动生成并正确工作的Clone、Equals、Equivalent、EqualityHash和EquivalenceHash。

+1,好主意。如果我可以再提供一点建议,切换到不可变对象(和不可变集合)可以轻松解决许多这类问题。一旦摆脱了可变状态,身份就不再重要。 - Anton Tykhyy
@AntonTykhyy:通常有一个与不变量相关联的恒定标识符是很有用的;将可变状态推到标识符级别通常比将所有变化限制在更高级别上更有用。例如,如果有一个包含某个州中所有汽车及其位置的列表,即使可以瞬间用一个相同但与ID“177-1234”相关联的新汽车替换它,想要持续知道该汽车位置的代码... - supercat
相比之下,如果汽车的位置保存在可变对象中,我们只需要一次性识别汽车对象,然后反复询问该汽车的当前位置,而不必反复向列表请求。 - supercat
我认为需要的是语言识别,将引用所封装的某些方面包括在类型系统中。这样的系统不应追求完美的表现力,但至少应处理常见情况。就像人们抱怨C++中的const-correctness一样,它主要是暴露了底层设计中可能以错误形式或编译器警告的形式显现出来的问题。 - supercat

1

克隆默认是深度的,这是命名约定,如果出于性能原因,复制构造函数可以是浅层的。

编辑:这个命名约定超越了边界,它适用于 .Net、Java、C++、Javascript 等等...实际源头超出了我的知识范围,但它是标准面向对象词汇的一部分,就像对象和类一样。因此,MSDN 不指定实现,因为这是单词本身的含义(当然,很多新手对面向对象语言不了解这一点,他们应该指定它,但再说一遍,他们的文档也相当节俭)。


你能提供更多细节吗?是哪种命名约定? - Jim Burger
2
那是不正确的。MSDN文档没有说明应该如何实现克隆功能。 - MagicKat

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