6

前几天我使用自定义方法来深度克隆对象,我知道你可以用不同的方式进行深度克隆(反射、二进制序列化等),所以我只是想知道:

Microsoft 没有在框架中包含深拷贝方法的原因是什么?

4

3 回答 3

8

这个问题比你想象的要困难得多,至少在一般情况下是这样。

对于初学者来说,副本不只是深或浅,它是一个频谱。

让我们想象一下,我们有一个字符串数组列表,我们想要复制它。

  • 我们从最浅的层次开始,我们只是将整个事物的引用复制到另一个变量。对从任一变量引用的列表所做的任何更改都会被另一个变量看到。

  • 所以现在我们去创建一个全新的列表来给第二个变量。对于第一个列表中的每个项目,我们将其添加到第二个列表中。现在我们可以修改从任一变量引用的列表,而不会被另一个变量看到。但是,如果我们获取列表的第一项并更改第一个数组中的字符串,这两个列表都会看到它!

  • 现在我们正在创建一个新列表,对于第一个列表中的每个数组,我们正在创建一个新数组,将底层数组中的每个字符串添加到新数组中,并将这些新数组中的每一个添加到新列表。现在我们可以改变任一列表中的任何数组,而不会看到变化。但是等等,两个列表仍然引用相同的字符串(毕竟它们是值类型;它们内部有一个用于数据的字符数组)。如果某个卑鄙的人出现并改变其中一个字符串怎么办(使用不安全的代码,您实际上可以做到这一点)!因此,现在您正在使用深层副本复制所有字符串。但是如果我们不需要这样做呢?如果我们知道没有人如此卑鄙以至于他们会改变字符串怎么办?或者,就此而言,应该由两个列表反映出来)。

然后当然会出现循环引用、类中的字段等问题,这些字段并不能真正代表它的状态(即,可能是缓存值或派生数据,可以根据需要由克隆重新计算)。

实际上,您需要实现每种类型IClonable或等效的类型,并拥有自己的自定义代码来克隆自身。对于一门语言来说,维护这将是很多工作,特别是因为有很多方法可以克隆复杂的对象。成本会相当高,而且好处(除了少数认为值得为其实现克隆方法的对象之外)通常不值得。作为一名程序员,你根据你知道你需要走的深度编写你自己的克隆类型的逻辑。

于 2012-10-04T18:57:25.227 回答
5

它类似于它在 C 和 C++ 中的工作方式(或不工作方式):

要进行深度复制,您实际上必须知道如何解释不同的数据。在普通情况下,浅拷贝(已提供)与深拷贝相同。但是一旦这不再是真的,它真的取决于实施和解释。没有一般的经验法则。

让我们以一个游戏为例:

  • NPC 对象有两个整数作为成员。一个整数代表它的生命值,另一个是它的唯一 ID。
  • 如果克隆 NPC,则必须保持生命值,同时更改唯一 ID。这是编译器/运行时无法自行确定的。您必须对此进行编码,本质上是告诉程序“如何复制”。

我可以想到两种可能的解决方案:

  • 添加关键字来表示无法复制的内容。虽然这听起来是个好主意,但它并不能真正解决问题。你可以告诉编译器UniqueID不能复制,但同时你不能定义这应该如何发生。即使你可以,你也可以...
  • 创建复制构造函数 (C++) 或复制/克隆对象的方法(C#,例如CopyTo())。
于 2012-10-04T18:41:43.137 回答
0

嗯..我的观点是:

A) 因为你很少希望副本真的很深
B) 因为框架不能保证知道如何真正有意义地克隆一个对象
C) 因为以一种简单的方式实现深度克隆很简单并且需要一个方法和几行使用反射和递归的代码

但我会尝试找到一篇涵盖该内容的旧 MSDN 文章

编辑:我还没有找到:(我仍然确定我在某处看到它,但我现在不能用谷歌搜索它。但是一些关于相关 ICloneable 和派生的有用链接:

http://blogs.msdn.com/b/brada/archive/2004/05/03/125427.aspx http://blogs.msdn.com/b/mrtechnocal/archive/2009/10/19/why-not -icloneable-t.aspx https://stackoverflow.com/a/3712564/717732

所以,由于我没有找到作者的话,让我扩展点:

A:因为很少你想让副本真的很深

你看,框架怎么能猜出它一般应该有多深呢?让我们假设完全深入并假设它已经实现。现在我们有了按成员克隆和完全克隆方法。尽管如此,在某些情况下,人们需要克隆我但不是根基。因此,他们发布了另一个问题,为什么全克隆无法切断原始碱基。或次生。等等。从 .Net 团队的角度来看,提供深度克隆几乎不能解决任何问题,因为我们这些用户仍然会因为看到一些部分工具并且很懒惰并且想要拥有一切而对此大发雷霆:)

B)因为框架不能保证知道如何真正有意义地克隆一个对象

尤其是一些带有句柄或类似本机 ID 的特殊对象,例如来自 Entity Framework、.Net 远程代理、COM 包装器等的对象:您可能会成功读取并克隆上层层次结构层,但最终,您会在下面的某个地方找到一些神秘的东西就像IntPtr你只知道你不应该复制的那样。大部分的时间。但有时你可以。但是框架的代码必须是通用的。深度克隆要么必须非常复杂,需要对具有特殊外观的类成员进行许多健全性检查,要么如果程序员在具有程序员不想分析的基类的东西上调用它,则会产生危险的结果。

B+) 另外,请注意,树中的基类越多,它们就越有可能具有一些参数化的构造函数,这可能表明直接复制不是一个好主意。可直接复制的类通常具有无参数的构造函数和属性可访问的所有可复制数据。

B++) 从框架设计者的角度来看,考虑到内存和速度,浅拷贝几乎总是非常快,而深拷贝正好相反。不允许开发人员自由地深度复制巨大的对象,这有利于框架和平台的声誉。无论如何,如果你的对象是轻量级和简单的,你会需要一个深拷贝,对吧?:) 不提供深拷贝会鼓励开发人员考虑深拷贝的需求,这通常会使应用程序更轻、更快。

C)因为以一种简单的方式实现深度克隆很简单,并且需要一个方法和几行代码,使用反射和递归

有了浅拷贝,真正写深拷贝有多难?没那么难!只需实现一个给定对象'obj'的方法:

pseudocode:
object deepcopier(object obj)
    newobject = obj.shallowcopy()
    foreach(field in newobject.fields)
        newobject.field = deepcopier(newobject.field)
    return newobject

好吧,就是这样。当然,字段枚举必须由反射执行,并且还必须读取/写入字段。

但是,这种方式非常幼稚。这有一个严重的缺陷:如果某个对象有两个指向同一个对象的字段怎么办?我们应该检测到它并进行一次克隆,然后将两个字段都分配给那个克隆。此外,如果某个字段指向的对象引用了另一个对象(...)也指向的某个对象 - 也可能只需要跟踪和克隆一次。另外,周期呢?如果在树深处的某个地方,一个对象有一个对根的引用?像上面这样的算法会很高兴地下降并重新复制所有内容,然后再一次,最终会被 StackOverflow 阻塞。

这使得克隆很难被跟踪,并且开始看起来更像是序列化。实际上,如果您的类是 DataContract 或 Serializable,您可以简单地对其进行序列化和反序列化以获得完美的深层副本 :)

深度克隆很难以通用的方式进行,除非您知道对象的含义及其所有字段的含义,并且知道哪些应该真正被克隆,哪些应该被统一。如果您作为开发人员知道这只是一个可以完全安全地进行深度克隆的数据对象,那么为什么不将其设为可序列化呢?如果您不能使其可序列化,那么您可能也无法对其进行深度克隆!

于 2012-10-04T18:37:21.720 回答