实现克隆
克隆对象
概述
.NET 对象有两种类型,值类型和引用类型。
-
值类型的变量,保存对象的位,并具有“赋值时复制”行为。
-
引用 (ref) 类型的变量是指向内存的指针。也就是说,当您创建 ref 类型的变量并进行赋值时,您正在创建另一个指向同一内存的指针。
本主题说明如何创建对象的副本并将其保存在变量中。
关于克隆的实现
克隆是复制对象的过程,即创建一个类的实例,与原始实例具有等效的信息。创建特定对象的副本,比分配新变量更复杂。
例如,下面的代码示例,创建了两个指向内存中同一个对象的变量:
[C#]
IPoint pointOne=new PointClass();
IPoint pointTwo=pointOne;
[VB.NET]
Dim pointOne As IPoint=New PointClass()
Dim pointTwo As IPoint=pointOne
请参阅以下两个变量的插图,它们指向内存中的同一对象:
复制对象比分配新变量更复杂
。
要复制 Point 对象:
创建具有与第一个 Point 可比数据的 Point 实例;使用 IClone 接口。
请参见以下代码示例:
[C#]
IClone clone=pointOne as IClone;
pointTwo=clone.Clone()as IPoint;
[VB.NET]
Dim clone As IClone
clone=Ctype(pointOne, IClone)
pointTwo=Ctype(clone.Clone(), IPoint)
请参见下图 IClone 界面:
克隆在内存中创建一个实例。
在 ArcGIS 中进行克隆
前面显示的技术(实现 IClone 接口)在 ArcGIS 中被广泛使用。例如,在应用程序将对象传递到属性页之前,它会克隆该对象。如果单击“确定”或“应用”按钮,则克隆对象的属性将设置到原始对象中。
在 ArcObjects 中克隆的另一个用途是通过专门返回对象副本的方法或属性,例如 IFeature.ShapeCopy 属性。
通过搜索 ArcObjects Help for .NET 开发人员中包含的示例,您可以找到有关如何使用克隆的其他示例。
复制成员(值和对象引用)
克隆操作的细节封装在类实现中。类规定了应该复制的成员以及应该如何复制它们。
每个实现了克隆的类,决定如何克隆它自己。
原始对象将被称为【克隆器(cloner)】,即执行克隆操作的对象。克隆过程产生的对象将被称为【克隆对象(clonee)】。
浅克隆和深度克隆
-
对于成员只包含值类型的简单对象,克隆过程相对简单。创建该类的一个实例,并将【克隆对象】的所有成员的值设置为等于【克隆器】的值。克隆对象独立于克隆器。
-
对于其成员包含对象引用的对象,克隆过程变得更加复杂。
-
如果克隆器仅将对象引用复制到克隆对象,则有时称为浅克隆(shallow clone)。
-
如果创建了每个引用对象的实例,并且克隆的成员被设置为引用这些新对象,则它被称为深度克隆(deep clone)。
请参见下图,其中显示了深度克隆和浅克隆:
-
ArcObjects 类使用了深度克隆和浅克隆。一个深度克隆的例子,graphic element 的克隆,其中引用的对象也被克隆 。graphic element 的成员 geometry 和 symbol 均被克隆;克隆对象的 geometry 和 symbol 属性完全独立于原始对象的 geometry 和 symbol 。
在某些情况下,将"对象引用"复制到新对象是合乎逻辑的(即浅克隆是合乎逻辑的)。例如,对要素类中的 geometry 使用浅克隆:
每个 geometry 都有一个对象引用来指示其坐标系(IGeometry.SpatialReference)。克隆 geometry 会生成一个对象,该对象具有对同一 【SpatialReference对象】的引用。在这种情况下,仅复制对象的引用(SpatialReference对象的引用)即可——因为一个要素类中的所有 geometry 都保持对同一 SpatialReference对象的引用 是合乎逻辑的【图层也是如此】。然后可以通过单个方法调用更改空间参考。
没有简单的规则,可以用来决定是应该复制对象引用还是应该克隆引用的对象。这是根据具体情况决定的,两种技术都可以包含在一个类中。
对于它的每个私有成员,一个类需要在浅层克隆或深层克隆之间做出适当的选择。
克隆"包含对象引用的对象"时要小心。在许多情况下,被引用的对象可以保存对更多对象的引用,而这些对象又可以保存对其他对象的引用,依此类推。
临时成员
在编写克隆方法时,请记住某些成员根本不应该被直接复制。例如,窗口句柄 (hWnd)、句柄设备上下文 (HDC)、文件句柄和 "GDI 资源"等。在大多数情况下,包含这种【特定于实例的信息】的对象,不应该被克隆。例如,工作空间(workspace)和要素类(feature class),它们具有特定于连接的信息。因此,不可克隆。概览窗口(overview window)和工具控件具( tool control)有窗口句柄,也不可克隆。
如果需要新实例,则应该从头开始创建对象。
如果您需要在此类对象上实现 IClone 接口,请确保从头开始创建任何特定于实例的信息,而不是复制特定于实例的值。
实现 IClone
如果您在自定义组件中实现克隆,请决定您希望如何复制包含在您的类中的信息
- 浅克隆还是深度克隆最适合每个成员
- 以及如何实现它。
[ComImport]
[TypeLibType(256)]
[InterfaceType(1)]
[Guid("9BFF8AEB-E415-11D0-943C-080009EEBECB")]
public interface IClone
{
//
// 摘要:
// Clones the receiver and assigns the result to *clone.
[MethodImpl(MethodImplOptions.InternalCall)]
[return: MarshalAs(UnmanagedType.Interface)]
IClone Clone();
//
// 摘要:
// Assigns the properties of src to the receiver.
[MethodImpl(MethodImplOptions.InternalCall)]
void Assign([In][MarshalAs(UnmanagedType.Interface)] IClone src);
//
// 摘要:
// Indicates if the receiver and other have the same properties.
[MethodImpl(MethodImplOptions.InternalCall)]
bool IsEqual([In][MarshalAs(UnmanagedType.Interface)] IClone other);
//
// 摘要:
// Indicates if the receiver and other are the same object.
[MethodImpl(MethodImplOptions.InternalCall)]
bool IsIdentical([In][MarshalAs(UnmanagedType.Interface)] IClone other);
}
Clone 方法
通常,有两种主要技术可以实现克隆。
首先,克隆器创建自己的一个实例,然后将自己所有成员复制到它上面。
第二种技术,利用对象的持久性机制功能,将对象临时保存到内存流中 ( ObjectStream ),然后从内存流中再加载回来。此技术要求对象实现 IPersist 和 IPersistStream 接口。在 .NET 中实现 IPersistStream 很复杂;因此,本主题不涉及此技术。
在 Clone 方法中,首先创建一个类的实例,即【克隆对象】。然后,再调用IClone.Assign() 将【克隆器】的属性复制到【克隆对象】上。最后,从 clone 方法返回对【克隆对象】的引用。以下代码示例是示例Clonable 对象的一部分:
[C#]
public IClone Clone()
{
ClonableObjClass obj=new ClonableObjClass();
obj.Assign(this);
return (IClone)obj;
}
[VB.NET]
Public Function Clone() As IClone Implements IClone.Clone
Dim obj As ClonableObjClass=New ClonableObjClass()
obj.Assign(Me)
Return CType(obj, IClone)
End Function
克隆应该创建一个类的实例。
Assign 方法
首先,如下代码,检查 src 看它是否指向一个有效的对象。如果不是,引发相应的错误(COMException)。
Assign 方法应该接收一个有效的类实例。
[C#]
public void Assign(IClone src)
{
//1. Make sure src is pointing to a valid object.
if (null == src)
{
throw new COMException("Invalid object.");
}
//2. Verify the type of src.
if (!(src is ClonableObjClass))
{
throw new COMException("Bad object type.");
}
//3. Assign the properties of src to the current instance.
ClonableObjClass srcClonable=(ClonableObjClass)src;
m_name=srcClonable.Name;
m_version=srcClonable.Version;
m_ID=srcClonable.ID;
//Use shallow cloning (use a reference to the same member object).
m_spatialRef=srcClonable.SpatialReference)
}
[VB.NET]
Public Sub Assign(ByVal src As IClone) Implements IClone.Assign
'1. Make sure src is pointing to a valid object.
If Nothing Is src Then
Throw New COMException("Invalid object.")
End If
'2. Verify the type of src.
If Not (TypeOf src Is ClonableObjClass) Then
Throw New COMException("Bad object type.")
End If
'3. Assign the properties of src to the current instance.
Dim srcClonable As ClonableObjClass=CType(src, ClonableObjClass)
m_name=srcClonable.Name
m_version=srcClonable.Version
m_ID=srcClonable.ID
'Use shallow cloning (use a reference to the same member object).
m_spatialRef=spatialRef=srcClonable.SpatialReference)
End Sub
前面的 Assign 代码对 ISpatialReference 进行了浅层克隆。其实,也可以对它执行深度克隆。请参见以下代码示例:
[C#]
IClone cloned=srcClonable.SpatialReference as IClone;
if (null != cloned)
{
m_spatialRef=(ISpatialReference)cloned.Clone();
}
[VB.NET]
Dim cloned As IClone=CType(srcClonable.SpatialReference, IClone)
If Not cloned Is Nothing Then
m_spatialRef=CType(cloned, ISpatialReference)
End If
如果成员对象不支持 IClone,则创建一个对象,并根据源对象的现有属性来设置其属性。
在对 Assign 方法进行编码时,需要考虑哪些成员适合浅克隆,哪些成员适合深度克隆,以及考虑某些成员根本就不适合克隆。
Assign 方法并不一定要包管“类的所有成员”,有时由客户端代码来设置将更为合适。例如,考虑 RandomColorRamp 如何实现 Assign 方法:克隆对象将具有与克隆器相同的 MinSaturation、MaxSaturation、MinValue、MaxValue、StartHue、EndHue、UseSeed、Seed 和 Name。但是,RandomColorRamp 的 Assign 方法不会复制 Size 的值或调用 CreateRamp 方法。这意味着 Color Ramp 没有 Colors 数组,此时不能在渲染器中使用。在调用 Assign 之后,客户端必须通过设置其 Size 属性并调用其 CreateRamp 方法来设置 RandomColorRamp 的 Colors 数组。
编码 Assign 方法时的另一个考虑因素,应该是克隆器和克隆对象的当前状态。在进行属性分配之前,清除克隆器持有的所有状态信息。在这种情况下,您可能需要添加一个内部初始化函数来将类的值设置为已知的初始状态。然后可以从您的类初始化函数中调用此函数。
在执行克隆之前,清除或重新初始化某些成员变量,以确保结果是可信的克隆。
保证深度克隆
要验证对象的 ref 成员的深度克隆,请考虑使用 ObjectCopy 对象。它提供了一种机制来复制对象,该机制使用了对象的持久性机制 (IPersistStream) 【对象被写入一个临时流,然后从该流重新水合到新实例中】。这种复制的过程也被称为深度克隆,因为会复制对象包含的所有子对象。
甚至在对象支持 IClone 的情况下,您仍可使用 ObjectCopy,因为它会执行对象的完整复制或深度克隆。
请参见以下代码示例:
[C#]
//Deep clone the spatial reference using an ObjectCopy.
if (null == srcClonable.SpatialReference)
m_spatialRef=null;
else
{
IObjectCopy objectCopy=new ObjectCopyClass();
object obj=objectCopy.Copy((object)srcClonable.SpatialReference);
m_spatialRef=(ISpatialReference)obj;
}
[VB.NET]
'Deep clone the spatial reference using an ObjectCopy.
If Nothing Is srcClonable.SpatialReference Then
m_spatialRef=Nothing
Else
Dim objectCopy As IObjectCopy=New ObjectCopyClass()
Dim obj As Object=objectCopy.Copy(CObj(srcClonable.SpatialReference))
m_spatialRef=CType(obj, ISpatialReference)
End If
要使用 ObjectCopy 深度克隆对象,该对象必须实现 IPersistStream 接口。
IsEqual 方法
用于比较克隆器(this)和克隆对象(other),看看所有成员的值是否相等;如果所有成员都相等,则返回 true。请参见以下代码示例:
[C#]
public bool IsEqual(IClone other)
{
//1. Make sure the "other" object is pointing to a valid object.
if (null == other)
throw new COMException("Invalid object.");
//2. Verify the type of "other."
if (!(other is ClonableObjClass))
throw new COMException("Bad object type.");
ClonableObjClass otherClonable=(ClonableObjClass)other;
//Test that all the object's properties are the same.
if (otherClonable.Version == m_version && otherClonable.Name == m_name &&
otherClonable.ID == m_ID && ((IClone)otherClonable.SpatialReference).IsEqual
((IClone)m_spatialRef))
)return true;
return false;
}
[VB.NET]
Public Function IsEqual(ByVal other As IClone) As Boolean Implements IClone.IsEqual
'1. Make sure that "other" is pointing to a valid object.
If Nothing Is other Then
Throw New COMException("Invalid object.")
End If
'2. Verify the type of "other."
If Not (TypeOf other Is ClonableObjClass) Then
Throw New COMException("Bad object type.")
End If
Dim otherClonable As ClonableObjClass=CType(other, ClonableObjClass)
'Test that all the object's properties are the same.
If otherClonable.Version=m_version AndAlso _
otherClonable.Name=m_name AndAlso _
otherClonable.ID=m_ID AndAlso _
CType(otherClonable.SpatialReference, IClone).IsEqual(CType(m_spatialRef, IClone)) Then Return True
Return True
End If
Return False
End Function
如果属性包含支持 IClone 的对象引用,则在成员对象上使用 IClone.IsEqual 来评估它是否等于传入引用的成员对象,其他。记得检查对象支持的所有接口的所有成员。
IsEqual 应该确定两个不同的对象是否具有可以被视为等效的值。
你决定你的班级认为什么是相等的值。如果两个 IColor 成员具有相同的红色、绿色、蓝色 (RGB) 值,您可能会认为它们是相等的,即使一个是 RGB 颜色,一个是青色、品红色、黄色和黑色 (CMYK) 颜色。
IsIdentical 方法
要实现IsIdentical,请比较接口指针以查看克隆器 (this) 和克隆器 (other) 是否指向内存中相同的底层对象。请参见以下代码示例:
[C#]
public bool IsIdentical(IClone other)
{
//1. Make sure the "other" object is pointing to a valid object.
if (null == other)
throw new COMException("Invalid object.");
//2. Verify the type of "other."
if (!(other is ClonableObjClass))
throw new COMException("Bad object type.");
//3. Test if the other is "this."
if ((ClonableObjClass)other == this)
return true;
return false;
}
[VB.NET]
Public Function IsIdentical(ByVal other As IClone) As Boolean Implements IClone.IsIdentical
'1. Make sure the "other" is pointing to a valid object.
If Nothing Is other Then
Throw New COMException("Invalid object.")
End If
'2. Verify the type of "other."
If Not (TypeOf other Is ClonableObjClass) Then
Throw New COMException("Bad object type.")
End If
'3. Test if the other is "this."
If CType(other, ClonableObjClass) Is Me Then
Return True
End If
Return False
End Function
IsIdentical 方法用于比较接口指针,以查看它们是否引用相同的底层对象。请参见下图: