为什么.NET框架不提供深度复制对象的方法?

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

微软在框架中不包含深层复制方法的原因是什么?

问题比你想象的要困难得多,至少在一般情况下如此。

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

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

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

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

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

然后当然存在诸如循环引用之类的问题,类中的字段实际上并不代表它的状态(即可能是缓存的值或者可以根据需要重新计算的派生数据)。

实际上,您需要让每个类型都实现IClonable或一些等效的,并拥有自己的自定义代码来克隆自己。 这对于维护语言来说需要做很多工作,特别是因为有很多方法可以克隆复杂的对象。 成本会非常高,并且(在少数几个被认为值得实现克隆方法的对象之外)的好处通常是不值得的。 作为程序员,您可以根据自己知道的深度来编写自己的逻辑来克隆类型。

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

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

让我们用一个游戏作为一个简单的例子:

  • NPC对象有两个整数作为成员。 一个整数表示其健康点,另一个是其唯一ID。
  • 如果克隆NPC,则必须在更改唯一ID时保持健康状况。 这是编译器/运行时无法自行确定的。 你必须编写这个代码,基本上告诉程序“如何复制”。

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

  • 添加关键字以表示无法复制的内容。 虽然这听起来不错,但它并没有真正解决问题。 您可以告诉编译器不能复制UniqueID ,但同时您无法定义如何发生这种情况。 即使你可以,你也可以……
  • 创建复制构造函数(C ++)或复制/克隆对象的方法(C#,例如CopyTo() )。

嗯..我的观点是:

A)因为很少你想让副本真的很深
B)因为框架无法保证知道如何真正有意义地克隆一个对象
C)因为以一种简洁的方式实现深度克隆很简单,并且使用reflection和递归采用一种方法和几行代码

但我会尝试找到一篇旧的MSDN文章

编辑:我没有找到:(我仍然确定我在某个地方看到它,但我现在不能google-it-out ..但是有关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

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

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

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

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

特别是对于一些具有句柄或类似本机ID的特殊对象,例如来自Entity Framework,.Net Remoting Proxies,COM-wrappers等的特殊对象:您可能成功地读取和克隆上层类层次结构层,但最终,在下面的某处您会发现一些神秘的东西像IntPtr那样你只知道你不应该复制。 大部分的时间。 但有时你可以。 但框架的代码必须是通用的。 深度克隆要么必须严格复杂,要么对特殊的类成员进行许多健全性检查,否则如果程序员在具有程序员不关心分析的基类的东西上调用它,就会产生危险的结果。

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

B ++)从框架的设计者角度来看,考虑到内存和速度问题,浅层复制几乎总是非常快,而深度复制恰恰相反。 不允许开发人员自由地深层复制大型对象,这对框架和平台的声誉是有益的。 无论如何,如果你的对象轻巧简单,你需要一个深层复制吗? :)不提供深层复制可以鼓励开发人员考虑深层复制的需要,通常会使应用程序更轻松,更快。

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

拥有浅层副本,实际编写深层副本有多难? 不是那么难! 只需实现一个给定对象’obj’的方法:

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

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

但是,这种方式非常有说服力。 这有一个严重的缺陷:如果某个对象有两个字段指向同一个另一个对象怎么办? 我们应该检测它并进行克隆,然后将两个字段分配给该克隆。 此外,如果某个字段指向的对象引用了某个也被另一个对象(…)指向的对象 – 也可能只需要跟踪和克隆一次。 还有,周期怎么样? 如果在树的深处,某个对象有一个返回根的引用? 像上面这样的算法很乐意下降并且会再次重新复制所有内容,然后再次重新复制,最终会被StackOverflow扼杀。

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

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