深复制和浅复制

时间:2022-08-23 14:30:57

在现实生活中,无论您采取何种立场,克隆都是一个非常微妙的论题。而对于软件,克隆有时却可成为极有价值的技术。事实上,您自己也经常会复制运行对象的实例,生成几乎完全相同的对象,并以非常独立的方式通过代码管理这些对象。

这并不是 .NET 或任何其它框架的特别之处,这些框架包括 Microsoft 基本类库 (MFC)、活动模板库 (ATL) 或 ActiveX® 数据对象 (ADO) 等等。在所有语言和编程上下文中,一个最普通的需求就是利用同一对象的现有实例创建另一个对象。

正如现实生活一样,软件克隆也没有一定的、被普遍接受的规则。任何对象克隆都必须经过一系列试验,而且总是会出现一些未知的、无法预测的结果。因此,不是所有对象(无论何种框架)都可以克隆,克隆行为可能随对象的不同而不同。以下是一些示例:

通过 MFC 中的类 constructor 或 C 运行时库函数 strdup(),可以快速轻松地复制字符串。一旦获得了副本,就可以将它和原始字符串当作两个完全不同的元素,而完全没有必要考虑获得该字符串的途径。但如果使用“ADO 记录集”,则表现出的特性就大不一样。

在 .NET 中,通过 ICloneable 接口为克隆提供支持。一些对象本身可以实现克隆,而另一些对象的克隆能力仅限于“浅复制”。对于这些对象,只能手动实现“深复制”。(将在随后以及第二部分提供与此有关的详细信息。)

本篇文章分为两部分,我们首先回顾一下 ADO 和 ADO.NET 对克隆的支持。然后我们将探讨一个著名案例,这就是最近新闻中所有关于克隆人类的话题—“Dolly”案例。

您可能已经知道,Dolly 是我曾试图在 Beta 1 代码中复制的第一个“DataTable”对象的名称。

首先了解一下在 ADO 中克隆表的特点。

记录集克隆在 ADO 应用中的利与弊

使用 Clone 方法可以轻松地克隆 ADO 记录集。如果您想复制现有的记录集(即,想得到一个使用其它变量名但内容相同的记录集),可以使用 Clone。当然,使用 Clone 要比通过连接和命令对象来初始化并填充新的记录集更快。

实际上,从克隆过程得到的是“指向相同数据的指针”,可以通过另外一个 Recordset 对象以编程方式获得此指针。这样做既提高了效率,同时也带来了一些负面影响。简而言之,一旦具有了 Recordset 副本,您便不能随意使用这两个实例,而是必须注意某些特殊情况,并极为谨慎地进行处理。

“克隆记录集”的优点之一是创建和使用它所需的开销较低。一个克隆记录集只是一个引用原始记录集数据的对象变量。另外,克隆记录集单独拥有一个缓冲区,用于跟踪记录书签。换句话说,可以单独对它们进行浏览,任何时候这两个记录集的当前位置都可以不同。另外,对关闭和过滤操作而言,原对象和克隆对象是相互独立的。

克隆也具有一些缺点(必须意识到这一点),即:无论克隆时原对象的指针位于何处,最初克隆对象的记录指针总是定位在第一个记录上;此外,进行克隆时,应用于原对象的任何过滤掩码都将丢失。不过,可以通过两个附加指令解决这两个问题:

Set rsClone = rsOrig.Clone 
rsClone.Move rsOrig.AbsolutePosition
rsClone.Filter = rsOrig.Filter

但必须注意,记录定位依赖于所使用的游标类型,如果试图使用默认的、向前移动游标在记录集内移动到一个随机位置,则会出错。

下图说明了既允许原对象和克隆对象使用个人书签、同时又共享数据的体系结构。

深复制和浅复制

图 1:原始和克隆记录集最初指向相同的数据

如果您需要维护多个“当前”记录,而又不想在一个记录集内来回移动以查找标有书签的记录,则克隆无疑是理想的解决方案。

当在两个记录集的其中之一内编辑记录时,会发生什么情况呢?由于原对象和克隆对象同步进行更新,因此,两个记录集都将检测到所做的更改。如图中体系结构所示,毫无疑问,它们正在共享相同的数据缓冲区。

当然这可能会导致一些严重问题,不过情况并不总是这样。当更改一个记录集中的数据时,其所有克隆对象都将收到通知,就好象这些对象直接参与更新过程一样。而这正是由于它们共享相同的数据缓冲区所致。

任何更改所引发的事件(例如 WillChangeFieldFieldChangeComplete)都会将记录集以及相关字段的一些信息传递给事件处理程序。尽管如此,该记录集也仅仅是对发生更改的记录集的引用。换句话说,克隆对象通过事件接收的记录集内的当前记录可能与克隆对象本身的当前记录不同。跨多个克隆对象的事件不会自动重定位记录指针。虽然本质上这并不是坏事,但应该牢记这一事实。

仅当从原对象或克隆对象调用方法 Requery 时,将它们连接在一起进行更新的链接才会断开。Requery 通过重新执行最初指定的查询命令来刷新 Recordset 对象中的数据。

Requery 将其数据集给予克隆对象,同时将其从以前共享的数据集断开。结果,原对象和克隆对象指向不同的数据集。对原始 Recordset 执行 Requery 时,情形也有所不同。下图对其进行了说明。

深复制和浅复制

图 2:对克隆对象调用 Requery 将产生自己的数据集

注意,Requery 仅断开原对象与调用它的克隆对象之间的连接。从同一原始 Recordset 创建的所有其它克隆对象则不受此操作影响,并继续共享数据。

深复制和浅复制

只有毫无经验的程序员才会认为只需简单地将对象分配给新变量便可实现对象复制。以下代码并不复制记录集,而只是为同一对象定义新的指针。

Set rs2 = rs1

此类操作通常称为“浅复制”,即部分复制。它并不复制对象的整个结构,而仅复制其顶层接口。“顶层接口”包含的内容并不是在所有上下文中都是一成不变的,而是随框架的不同而不同。它可以只是指向当前实例的指针或包含对象本身,但不能包括它的子对象。

当需要克隆对象以获取一个完整的、独立的、功能相同的副本时,可以使用“深复制”。

对 ADO 记录集而言,当复制变量时,进行的是浅层的浅复制;而调用 Clone 时,进行的是深层的浅复制。这时,您不会得到两个完全独立的对象,因为它们仍共享数据缓冲区。只有使用以下代码,rs2 变量才能成为原始 rs1 ADO Recordset 对象的真正意义的深复制对象。

Set rs2 = rs1.Clone
rs2.Requery

浅复制和深复制是在 .NET 文档中经常使用的术语,用于解释在框架内如何进行克隆。

克隆在 .NET 应用中的利与弊

.NET 中有两种类型的克隆:值和引用。前者包括原始类型(枚举和结构体),在堆栈中进行分配。后者包含类和数组,在堆中进行分配。本质上,值类型携带它的所有内容。而引用类型则指向内存缓冲区,在其中维护它的部分或所有内容。

在 .NET 中,可以使用这两种方式复制对象,但不同方式提供的功能不同。可以对 .NET 对象进行浅复制或深复制。

浅复制仅得到对象的一个副本。如果对象结构比较复杂,并包含对子对象的引用,则这些子对象将不会被复制。任何子对象都将引用浅复制中的原对象。(在克隆的 ADO 记录集中,对数据而言或多或少是这种情况,但书签和过滤器则情况不同。)

深复制将得到对象的完整副本。将得到一个全新的对象,在这个新对象中,所有被原对象直接或间接引用的内容都会被复制。

.NET 框架的根对象(即 Object 对象)具有一个名为 MemberwiseClone 的方法,该方法可创建当前对象的浅复制。

protected object MemberwiseClone();

此方法表示 .NET 对象实现浅复制的内置功能。此方法是受保护的,无法覆盖。

如果认为标准浅复制机制不适合您的对象,可以采用新方法克隆/复制它们,或实现 ICloneable 接口。如果需要的不仅仅是标准克隆(以前所述的浅复制),则必须采取某些措施。

ICloneable 接口

要使 .NET 类成为可克隆的类时,通常采用 ICloneable 接口。注意,实际上完全可以采用自定义接口或自己开发一种克隆方法。但是,由于 ICloneable 是克隆对象的标准接口,因此可克隆的对象应使用此接口。

ICloneable 只包含一个名为 Clone 方法。通过此方法可以实现何种操作取决于试图克隆的对象。通常可以在以下情况下使用它:提供深复制功能,或要使用 MemberwiseClone 不具备的功能。

浅复制或深复制时都可以实现 Clone。为保证一致性,请确认提供的是对象当前实例的副本。

.NET 框架的特点是包含一些已实现 ICloneable 的对象。此外还提供了 ArrayHashTableQueueString、各种 GDI 对象、XmlNavigator,以及 XmlNode

克隆 ADO.NET 对象

可克隆对象集由一组 ADO.NET 对象构成,包括 DBCommandDBConnectionDBDataSetCommandDataTableMappingSQLParameter。所有这些对象以及派生类都具有 Clone 方法,此方法确保了可以实现精确的、或深或浅的复制过程。

DBDataSetCommand 类的 Clone 方法通过 SQLDataSetCommand 和 ADODataSetCommand 类实现。它并不对类进行完整复制,而是返回一个新对象,该对象将复制尽可能多的信息,而不会对共享资源(如连接)产生太大的影响。

DBDataSetCommand 嵌入的对象不都是深复制对象。配置对象如 TableMappingsMissingSchemaActionMissingMappingAction 被复制。而命令对象(如 SelectDBCommandInsertDBCommandDeleteDBCommandUpdateDBCommand)情况就不尽相同了。

更准确地讲,DBDataSetCommand 的 Clone 方法实际上将对它所包含的任意 Command 对象调用 Clone 方法。但由于可扩展性原因,DBCommand 对象的克隆不是完整的深复制。事实上,这些命令的活动连接未被复制,而是进行了共享。

显而易见,这种对纯克隆原则的明显违背对于应用程序却大有裨益,因为可以将克隆的 DBDataSetCommand 用于与原对象相同的连接。如果您只是需要一个新连接,则应替换 Command 对象的 ActiveConnection 属性。

讨论 Dolly 表

不是所有的 ADO.NET 类都为克隆提供扩展支持。事实上,并非所有这些类都实现 ICloneable。特别是在 Beta 1 版本中,DataTableDataRowDataColumn 并不提供特殊克隆功能。

DataSet 对象则稍有不同。它未实现 ICloneable 接口,但提供了 CloneCopy 两种方法,以提供两种克隆级别。需要特别说明的是,Clone 只复制 DataSet 的结构,即表、关系和约束;而 Copy 则同时克隆架构和数据,对 DataSet 的内容进行真正的深复制。

在 Beta 2 中,DataTableDataRow 将提供 Clone/Copy 方法对,类似于 DataSet

使用典型的“断开连接的应用程序”(顺便提一下,它是 .NET 中首选的一种应用程序)的 DataTable 时,需要用到子表。乍看起来,它并不象是很难编写的代码。但是仔细研究后发现,这是一项非常艰巨的任务,需要对一个不熟悉的 DataTable —这个可怜的 Dolly 表进行一系列操作。

我们下次再讨论这个问题!

对话框:DataSet 的用途是什么?

最近某些 ADO.NET 文档提到用 DataSets 从数据源中提取数据。如果只需要遍历记录集合以创建一个表,为什么不使用更快捷的 DataReader 呢?

需要维护跨多页请求的记录时,如果没有事先将 DataSet 对象保存至会话或应用程序中,应如何完成此任务?

什么时候使用哪一个?DataSet 能够为我做些什么?DataSet 不能为我做什么?

DataReaderDataSet 是迥然不同的两种对象。前者工作时处于连接状态,允许以只向前移动的、只读方式查询一个表。换句话说,DataReader 就是由以下代码构成的对象:

While Not rs.EOF
' do something
rs.MoveNext
Wend

如果不需要添加、删除或修改记录,或者不需要来回移动,为什么要使用如此笨重的对象来完成如此简单的任务呢?对于 .NET 而言,这就是 DataReader 的用途。对于此类任务,DataSet 不是合适的对象。

DataSets 是驻留在内存中的数据容器,用于将数据和规则“打包”。可以将它们保存在内存中,作为客户机和数据源之间的一种中间服务器端缓存。可以轻松地将其内容序列化为 XML 并将它发送到一个磁盘文件,或通过 HTTP 发送到一个连接的客户机。更重要的是,整个过程中您都无需考虑目标平台和公司防火墙的影响。

在 WinForm 应用程序和 Web 服务中,DataSet 是在中间层与客户端应用程序之间传递的关键对象。因此,DataSet 主要用于存储跨多个请求的数据。所以理应将其保存到会话或应用程序中。

如果您只需要在单个请求内访问、读取和“使用”数据,则 DataSet 可能并不是最理想的工具。但另一方面,DataReader 是一个只读的、仅向前移动的游标。如果它不能满足您的需求,则可使用 DataSet。但它的最佳用途仍是存储会话范围的数据。

 


Dino Esposito 的工作单位是 Wintellect(英文),深复制和浅复制他负责 ADO.NET 和 ASP.NET 的培训和咨询工作。他是 VB-2-The-Max(英文)的创始人之一,深复制和浅复制也为 MSDN 杂志的 Cutting Edge 专栏撰写文章。他的电子邮件地址是 dinoe@wintellect.com