Delphi对象克隆技术

Delphi交流QQ:2609715119

关键字: 原型模式,对象克隆,Prototype,Clone

概述:
在这篇文件中,讲述原型模式定义、结构、应用、实现。深入讨论了Delphi中原型模式的实现方法。特别研究了原型模式的核心技术——对象克隆技术在Delphi中的实现途径。本文还探讨了对象引用复制和对象克隆的区别,以及VCL的持久对象机制、对象流化技术。

1、原型模式解说
原型模式通过给出一个原型对象来指明所要创建对象的类型,然后克隆该原型对象以便创建出更多同类型的新对象。
例如:在Delphi的IDE中,我们为设计窗体拖放了一个按钮对象。为了快速创建更多的同样字体和尺寸的按钮对象,我们可以复制该按钮(使用菜单Copy菜单或快捷键Ctrl+C),并在设计窗体多次粘贴(使用菜单Paste菜单或快捷键Ctrl+V。
设计窗体中的按钮对象是用于我们应用程序的,而IDE中提供的按钮对象创建方法(复制和粘贴)则是属于Delphi架构的。我们通过复制创建一个按钮对象时,不需要知道Delphi是如何实现的。
所 要说明的是,虽然我们使用的是类似文字处理中的复制和粘贴,但复制的决不是一个按钮对象的外观(字体和尺寸等),而是整个按钮对象,包括它的属性和方法。 所以,更严格讲,我们是克隆了这个对象,即得到一个和源对象一样的新对象。我们称这种被克隆的对象(比如按钮)为原型。只要系统支持克隆功能,我们就可以 任意克隆对象。
由此可见,原型模式适用于系统应该与其对象的创建、组合及显示时无关的情况,包括:
-当要实例化的类是在运行时刻指定时,例如,通过动态载入。
-当类实例只是少数不同组合状态其中之一时,这时比较好的方式在适当的状态下使用一些组合的原型并复制他们,而不是人工的继承这些类。
-避免建立工厂类等级结构平行产出类等级结构时。
假 设一个系统的产品类是动态加载的,而且产品类具有一定的等极结构。这个时候如果采取工厂模式的话,工厂类就不得不具有一个相应的等级。而产品类的等级结构 一旦变化,工厂类的等级结构就不得不有一个相应的变化。这对于产品结构可能会有经常性变化的系统来说,采用工厂模式就有不方便之处。
这时如果采取原型模式,给每一个产品类配备一个克隆方法(大多数的时候只需给产品类等级结构的基类配备一个克隆方法),便可以避免使用工厂模式所带来的具有固定等级结构的工厂类。
这样,一个使用了原型模式的系统与它的产品对象是如何创建出来的,以及这些产品对象之间的结构是怎样的,还有这个结构会不会发生变化,都是没有关系的。


2、Delphi对象的克隆
原型模式通过克隆原型对象来创建新对象,因此了解和掌握Delphi中对象的克隆是使用原型模式的关键。
在Delphi创建一个对象实际上就是把一个类进行实例化。例如要从TMan类创建一个名为Tom的对象,可以这样创建:

var Tom:TMan;
......
Tom:=TMan.Create;

以上语句完成了以下工作:
-声明TMan类型的变量Tom;
-为TMan类创建一个实例;
-将变量Tom指向创建的实例。
我们从中可以发现,对象变量和对象并不是一回事。对象是TMan类创建的一个实例,对象变量是该对象的引用。为了简单,在称呼上我们通常并不严格区分。但在使用时,务必分清对象引用和实际对象。
有时在使用对象时无需使用对象变量来区分某一对象,例如:

Factory.MakeTool(TMan.Create);

这里无需区分TMan的实例是Tom还是Jack。
但我们使用以下例子时,表示Tom和Jack分别引用了不同的TMan的实例,此时他们是两个对象。

var Tom,Jack:TMan;
......
Tom:=TMan.Create;
Jack:=TMan.Create;

但是如果接着使用以下语句:

Tom:=Jack;

此时Tom变量就不再引用Tom对象,而是引用Jack对象,这就好像Tom变成了Jack的另一个名字。当你找Tom时,找到的是Jack。所以这种方法只能复制对象的引用而不能克隆整个对象。
由 此我们了解到,对象是类的动态实例,对象总是被动态分配到堆上。因此一个对象引用就如同一个句柄或一个指针。但你分配一个对象引用给一个变量 时,Delphi仅复制引用,而不是整个对象。在Delphi中使用一个对象的唯一方法就是使用对象引用。一个对象引用通常以一个变量的形式存在,但是也 有函数或者属性返回值的形式。
Delphi中不像有的语言那样提供了对象克隆的功能(比如:Java有Object.clone方法),所以在Delphi中实现对象克隆的功能需要自己编写代码。
好在VCL的体系结构中,TPersistent类系下的对象可以通过覆盖Assign方法,实现克隆行为。TPersistent的Assign方法较常用于两个对象属性的复制。在Assign方法中可以完成对象属性、方法和事件的逐个复制。
Assign方法在TPersistent类中声明为虚方法,以便允许每个派生类定义自己的复制对象方法。如果派生类没有重写Assign方法,则TPersistent的Assign方法会将复制动作交给源对象来进行:

procedure TPersistent.Assign(Source: TPersistent);
begin
if Source <> nil then Source.AssignTo(Self) 
else AssignError(nil);
end;

由此可见, Assign方法实际上是调用AssignedTo方法来实现的,因此TPersistent的Assign方法很少被派生类所重载,但AssignTo却常被派生类根据需要重载。
如果由AssignedTo方法来实现复制,那么必须保证源对象的类已经重写了AssignedTo方法,否则将抛出一个AssignError异常:

procedure TPersistent.AssignError(Source: TPersistent);
var
SourceName: string;
begin
if Source <> nil then
  SourceName := Source.ClassName else
  SourceName := 'nil';
raise EConvertError.CreateResFmt(@SAssignError, [SourceName, ClassName]);
end;

那么在程序中是如何使用Assign方法实现对象克隆的呢?如果要让对象b克隆对象a,则可以考虑以下赋值操作:

......
var 
{
假设这里的TMyObject是TPersistent的派生类,
并实现了Assign方法。比如:TFont 。
}
a,b:TMyObject; 
begin
a:= TMyObject.create;
{ 关于对象a的代码}
...... 
{ 开始克隆对象a}
b:= TMyObject.create;
b.Assign(a);//对象b的属性和内容和对象a完全相同。
end;

由此可见,b:=a意味着b是a的引用,即两者是同一对象。如果写成b.Assign(a),那么b是仍然一个独立的对象,其状态与a相同,也就可以看成是b克隆了a。

 

 2003-9-18 14:35:52   

3、结构和用法
原始模式的结构如图所示。它涉及到三个参与者:
-抽象原型(Prototype)——声明一个接口以便克隆其本身。
-具体原型(ConcretePrototype)——实现克隆的操作以克隆本身。
-客户(Client)——请求原型克隆其本身以构建新的对象。
他们之间的合作关系为客户请求原型克隆复制其本身以构建新的对象。
原型模式的实现代码模板如示例程序 9 1所示。
示例程序 9 1原型模式的实现代码模板
unit Prototype;

interface

uses
 SysUtils, Windows, Messages, Classes, Graphics, Controls,
 Forms, Dialogs;

type

 TPrototype = class (TObject)
 public
   function Clone: TObject; virtual; abstract;
 end;

 TConcretePrototype1 = class (TPrototype)
 public
   function Clone: TObject; override;
 end;

 TConcretePrototype2 = class (TPrototype)
 public
   function Clone: TObject; override;
 end;

 TClient = class (TObject)
 private
   FPrototype: TPrototype;
 public
   procedure opteration;
 end;

implementation

{TClient }
procedure TClient.opteration(NewObj:TPrototype);
var
 NewObj:TPrototype;
begin
 {克隆一个对象}
 NewObj:=FPrototype.Clone;
end;

{TConcretePrototype1 
function TConcretePrototype1.Clone: TObject;
begin
{克隆的实现代码}
end;

{TConcretePrototype2}
function TConcretePrototype2.Clone: TObject;
begin
{克隆的实现代码}
end;

end.


前 面我介绍的原型模式通常用于原型对象数目较少且比较固定的情况,这种情况下原型对象的引用由客户对象自己保存。但是,如果要创建的原型对象数目不是很固 定,则可以采用下面介绍的注册形式的原型模式。在这种情况下,客户对象不用保存对原型对象的引用,而是另有原型管理器负责。
我们把负责注册原型对 象的参与者称为原型管理器(prototype manager)。原型管理者储存原型并依据一定的键值(或索引值)传回相对应的原型,它包含了原型对象的添加、删除等操作,并使每个原型对象相对应一个 键值(或索引值)。客户端可以在运行期改变或者浏览这个注册项。这样一来,客户端在系统中管理及扩充原型对象数量都无须撰写更多的程序代码。Delphi 的TObjectList类可以帮我们实现原型对象的注册和管理。
注册形式的原型模式结构图比前图 多了一个原型管理器(TPrototypeManager)。原型管理器负责创建具体原型类的对象,并记录每一个被创建的对象。


示 例程序 9 2给出了一个示意性实现的源代码。首先,抽象原型TPrototype声明了一个Clone方法,然后由具体原型TConcretePrototype1 和TConcretePrototype2分别实现该Clone方法。管理器TPrototypeManager为注册原型对象提供必要的方法,包括:新 增、获取、计数等。它实际上是通过Delphi的TObjectList自动实现这些功能的。
系统中的客户对象通常先创建一个新的原型对象,然后 克隆一份,注册并保存在原型管理器中。在注册形式的原型模式中,当客户对象克隆一个原型对象之前,客户对象先查看管理对象中是否已经存在有满足要求的原型 对象。如果有就直接从管理对象中取得该对象的引用;如果没有,则克隆并注册该原型对象。
------------------------
示例程序 9 2注册形式的原型模式实现代码模板
unit Prototype2;

interface

uses
 SysUtils, Windows, Messages, Classes, Graphics, Controls,
 Forms, Dialogs,Contnrs;

type
 TPrototype = class (TObject)
 public
   function Clone: TObject; virtual; abstract;
 end;

 TConcretePrototype1 = class (TPrototype)
 public
   function Clone: TObject; override;
 end;

 TConcretePrototype2 = class (TPrototype)
 public
   function Clone: TObject; override;
 end;

 TPrototypeManager = class (TObject)
 private
   FObjects: TObjectList;
 public
   constructor create; override;
   procedure add(AObject: TPrototype);
   function Count: Integer;
   function get(Index: Integer): TPrototype;
 end;

 TClient = class (TObject)
 private
   FPrototype: TPrototype;
   FPrototypeManager: TPrototypeManager;
 public
   constructor create; override;
   procedure opteration;
   procedure registerPrototype;
 end;

implementation

{TClient }
procedure TClient.opteration;
begin
//具体实现代码
end;

constructor TClient.create;
begin
 FPrototypeManager:=TPrototypeManager.Create;
end;

procedure TClient.registerPrototype;
var
 ClonedObj:TPrototype;
 i:integer;
begin
  //示意性代码
   FPrototype:=TConcretePrototype1.Create;
   ClonedObj:=TPrototype(FPrototype.Clone);
   FPrototypeManager.add(ClonedObj);
end;

{TConcretePrototype1 }
function TConcretePrototype1.Clone: TObject;
begin
//具体实现代码
end;

{TConcretePrototype2}
function TConcretePrototype2.Clone: TObject;
begin
//具体实现代码
end;

{TPrototypeManager}
constructor TPrototypeManager.create;
begin
 FObjects.Create;
end;

procedure TPrototypeManager.add(AObject: TPrototype);
begin
 FObjects.Add(AObject);
end;

function TPrototypeManager.Count: Integer;
begin
 result:=FObjects.Count;
end;

function TPrototypeManager.get(Index: Integer): TPrototype;
begin
 result:=TPrototype(FObjects.Items[Index]);
end;

end.


4、原型模式范例详解
在许多程序中我们需要允许用户自己定义他们喜欢的字体。几乎所有Delphi的用户界面控件(UI)都提供TFont属性,用于设置字体。另外Delphi还提供字体对话框方便可视化操作。通常在需要允许用户设置字体的地方,程序员可以写上这样一段代码:

   if  FontDialog1.Execute then
     Memo1.Font.Assign(FontDialog1.Font);

如 果在程序中有很多地方需要用户设置字体,这种代码就会出现在多处,并涉及到具体的用户界面控件,修改维护都很麻烦,而且用户也要费劲设置很多次字体。能不 能让用户只设置一次字体,然后复制到他们需要的各处?也就是说能不能先创建一个与使用字体界面无关的字体对象原型,然后在需要的地方克隆?我们不妨就用原 型模式来解决这个问题。
按照原型模式我设计的类图如图所示。演示界面TfrmClient是客户,它使用抽象类TPrototype_Font提 供的SetFont方法设置字体(原型对象)以及Clone函数克隆并返回克隆好的字体对象。但抽象类TPrototype_Font作为抽象原型仅仅是 一个接口,真正实现SetFont方法和Clone函数的是具体原型TPrototype_Font1和TPrototype_Font2。通过派 生,TPrototype_Font1类和TPrototype_Font2类可以提供不同的克隆产品和克隆实现方法。范例程序中我重点演示不同的克隆实 现方法。



示 例程序 9 3是原型字体程序的实现源码。在该程序中可以看到,我在TPrototype_Font1的Clone函数中调用了Assign方法来实现的字体克隆。 TFont实现了TPersistent的虚方法Assign,作为TFont派生类的TPrototype_Font1理所当然继承TFont的 Assign方法。

function TPrototype_Font1.Clone: TObject;
var
 Temp: TPrototype_Font1;
begin
 Temp:=TPrototype_Font1.Create;
 Temp.Assign(self);
 result:=Temp;
end;

如 果在某些类中没有Assign方法(或没有实现TPersistent的Assign方法),我们就不得不自己撰写Clone函数了。 TPrototype_Font2中的Clone函数就是自己编程实现克隆的示例。所谓克隆对象,实际上关键在复制该对象的状态,即对象的属性值(数据成 员变量的值)。因为有同一个类创建的对象之间的差异就在于这些属性值的不同。

function TPrototype_Font2.Clone: TObject;
var
 Temp: TPrototype_Font2;
begin
 Temp:=TPrototype_Font2.Create;
 Temp.Name:=self.Name;
 Temp.Size:=self.Size;
 Temp.Color:=self.Color;
 result:=Temp;
end;

但 是,有时候某些属性值并不是客户所需要的,所以在克隆时对对象的属性值进行取舍是允许的。TPrototype_Font2中的Clone函数中,我只复 制了影响字体外观的3个最主要的属性。这一取舍的后果是,克隆的新字体并不完全和原型字体一致,比如,不能反映出是否是斜体。这种克隆称为不完全克隆。
-------------
示例程序 9 4 原型字体程序的实现源码
unit PrototypeFont;

interface

uses
 SysUtils, Windows, Messages, Classes, Graphics, Controls,
 Forms, Dialogs;

type

 TPrototype_Font = class (TFont)
 public
   function Clone: TObject; virtual; abstract;
   procedure SetFont; virtual; abstract;
 end;
 
 TPrototype_Font1 = class (TPrototype_Font)
 public
   function Clone: TObject; override;
   procedure SetFont; override;
 end;
 
 TPrototype_Font2 = class (TPrototype_Font)
 public
   function Clone: TObject; override;
   procedure SetFont; override;
 end;
 

implementation


{TPrototype_Font1调用Assign方法实现的字体克隆。}
function TPrototype_Font1.Clone: TObject;
var
 Temp: TPrototype_Font1;
begin
 Temp:=TPrototype_Font1.Create;
 Temp.Assign(self);
 result:=Temp;
end;

procedure TPrototype_Font1.SetFont;
var
 FontDialog: TFontDialog;
begin
 FontDialog:= TFontDialog.Create(nil);
 try
   if  FontDialog.Execute then
     TFont(self).Assign(FontDialog.Font);
 finally
   FontDialog.Free;
 end;
end;

{TPrototype_Font2 通过自己编程实现的字体克隆。}
function TPrototype_Font2.Clone: TObject;
var
 Temp: TPrototype_Font2;
begin
 Temp:=TPrototype_Font2.Create;
 Temp.Name:=self.Name;
 Temp.Size:=self.Size;
 Temp.Color:=self.Color;
 result:=Temp;
end;

procedure TPrototype_Font2.SetFont;
var
 FontDialog: TFontDialog;
begin
 FontDialog:= TFontDialog.Create(nil);
 try
   if  FontDialog.Execute then
   begin
     self.Name:=FontDialog.Font.Name;
     self.Size:=FontDialog.Font.Size;
     self.Color:=FontDialog.Font.Color;
   end;
 finally
   FontDialog.Free;
 end;
end;

end.


在示例程序 9 4所示的客户程序中,我设计了一个演示界面。通过“设置字体”按钮可以设置原型字体,然后点击“<-克隆”按钮,可以将原型字体克隆到左边的文本编辑框中。如图所示。


注 意这里使用的多态和转型技术。TPrototype_Font的clone函数把克隆的对象向下转型为TObject传出,示例程序 9 4中再把返回的对象向上转型为TPrototype_Font。因为TFont是TPrototype_Font的基类,下面的写法是安全的。

Memo1.Font:=TPrototype_Font(FPrototype_Font1.clone);

示例程序 9 5 客户程序源码
unit MainForm;

interface

uses
 Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
 Dialogs, StdCtrls,PrototypeFont, ExtCtrls;

type
 TForm1 = class (TForm)
   Memo1: TMemo;
   btnSet1: TButton;
   btnClone1: TButton;
   btnClone2: TButton;
   Memo2: TMemo;
   btnSet2: TButton;
   Bevel1: TBevel;
   procedure FormCreate(Sender: TObject);
   procedure btnClone1Click(Sender: TObject);
   procedure btnSet1Click(Sender: TObject);
   procedure btnClone2Click(Sender: TObject);
   procedure btnSet2Click(Sender: TObject);
 private
   FPrototype_Font1: TPrototype_Font;
   FPrototype_Font2: TPrototype_Font;
 end;
 
var
 Form1: TForm1;

implementation



{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
begin
 FPrototype_Font1:=TPrototype_Font1.Create;
 FPrototype_Font2:=TPrototype_Font2.Create;
 Memo1.Lines.Add(
   '这里演示调用Assign方法实现的字体克隆。') ;
 Memo2.Lines.Add(
   '这里演示通过自己编程实现的字体克隆。') ;
end;

procedure TForm1.btnClone1Click(Sender: TObject);
begin
 Memo1.Font:=TPrototype_Font(FPrototype_Font1.clone);
end;

procedure TForm1.btnSet1Click(Sender: TObject);
begin
 FPrototype_Font1.SetFont;
end;

procedure TForm1.btnClone2Click(Sender: TObject);
var
 Prototype_Font2: TPrototype_Font2;
begin
 Memo2.Font:=TPrototype_Font(FPrototype_Font2.clone);
end;

procedure TForm1.btnSet2Click(Sender: TObject);
begin
 FPrototype_Font2.SetFont;
end;

end.

5、使用Delphi流技术克隆对象实现原型模式
在 Delphi中,流对象与流化存储技术不仅是Delphi可视化设计实现的核心,它也为实现原型模式提供了更为方便、简单和通用的对象克隆方法。尽管流化 存储所涉及的存储媒介十分广泛,但在各对象的接口上得到了统一,使程序的存储操作变得十分方便、简单,从而使程序员能站在更高层面上进行对象存取的有关编 程工作而无需考虑存储介质的具体差异。
在VCL中。从TPersistent类开始提供了一个接口,引入了对象的可赋值性和流化 (assignment and streaming capabilities)等性质。TPersistent类是抽象类,没有实例对象。但该类实现了对象公布(Published)属性的存取,即在该类 及其派生类中声明为Published的属性、方法和事件等可在设计期时显示在Object Inspector窗中,能在Object Inspector中对对象的Published属性进行设计期的设计,并可将设置的值存到窗体或数据模块的DFM文件中。在程序运行期,对象将被初始化 为设计期所设置的状态。例如,在Delphi窗体上设计的按钮对象,在程序运行期该对象将被初始化为设计期所设置的状态。
在编程中为了实现流化, 我们需要使用TStream的派生类。TStream是所有流类的抽象基类,它继承自TObject。它的派生类主要用于对文本、内存、数据库的Blob 字段、数据压缩等进行操作。由于TStream与具体的存储无关,派生类却与存储媒介紧密相关,因此每个子类都必须实现与具体存储媒介相关的方法,如磁 盘、内存等。通过流的写方法我们可以把对象转化成位模式保存在内存或文件中;同样,通过流的读方法我们可以把保存在内存或文件中的位模式重新转化成对象。 通常把对象写到流里的过程称为串行化,而把对象从流中读出来的过程称为并行化。通过对象的一进一出,实际上就实现了对象的克隆。
需要注意的是,能够被流化的对象必须是TPersistent的派生类。一般Delphi的组件(TComponent的派生类)都是支持流化的。用户要使自己定义的类能够支持流化,通常至少应该使其继承自TPersistent。
————————————————————————————————————————
参阅:需要进一步了解Delphi对象流化机制以及VCL相关的类,请参阅我著的《Delphi面向对象编程思想》(机械工业出版社2003年9月)。
————————————————————————————————————————


下面我通过一个例子来演示说明如何使用流克隆对象实现原型模式。这个例子有点类似前图所示的通过Delphi的IDE复制和粘贴按钮对象。下图是演示程序的运行界面。我们点击“克隆对象”按钮,就会把一个文本框克隆到窗体上。


为此,我使用原型模式设计了下图的类结构。与前面原型模式示例程序不同的是,这里的TMemoPrototype类Clone方法利用了对象流化来完成对象的克隆。


function TMemoPrototype.Clone: TObject;
var
 tmp:TMemoPrototype;
 TmpStream:TStream;
begin
 WriteComponentResFile('MemoPrototype.dat',self);
 tmp:=TMemoPrototype(ReadComponentResFile('MemoPrototype.dat',nil));
 result:=tmp;
end;

这 里我们使用了Delphi的Classes单元中两个全局函数WriteComponentResFile和ReadComponentResFile来 完成对象的串行化和并行化过程,前者将对象以位模式写到资源文件MemoPrototype.dat中,后者从资源文件 MemoPrototype.dat中读出对象,每读出一个对象就好像克隆出了一个对象。这两个函数封装了文件流(TFileStream)的操作过程, 使我们编程简单化,不过从他们的实现代码中(示例程序 9 6),我们还是可以看出这一流化过程的。
-------------
示例程序 9 6 WriteComponentResFile和ReadComponentResFile的实现代码
procedure WriteComponentResFile(const FileName: string; 
Instance: TComponent);
var
 Stream: TStream;
begin
 Stream := TFileStream.Create(FileName, fmCreate);
 try
   Stream.WriteComponentRes(Instance.ClassName, Instance);
 finally
   Stream.Free;
 end;
end;

function ReadComponentResFile(const FileName: string;
Instance: TComponent): TComponent;
var
 Stream: TStream;
begin
 Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
 try
   Result := Stream.ReadComponentRes(Instance);
 finally
   Stream.Free;
 end;
end;

于是,在使用克隆对象的演示窗体中,我们通过点击“克隆对象”按钮,触发了以下按钮事件:

procedure TForm1.btnCloneClick(Sender: TObject);
var
 tmp:TMemoPrototype;
begin
 tmp:=TMemoPrototype(FPrototype1.clone);
 tmp.Name:='Memo'+IntToStr(n);
 tmp.Parent:=self;
 tmp.Lines.Add('克隆文本框之'+IntToStr(n));
 tmp.Left:=tmp.Left+30*n;
 tmp.Top:=tmp.Top+30*n;
 inc(n);
end;

通过克隆原型对象的方法,我们将方便地得到TMemoPrototype的实例。这些对象有着和原型对象一样的状态,比如:相同的外观和字体。(为了便于演示,我有意调整了他们的位置,否则窗体上的文本框会重叠在一起。)
由此可见,流化对象的强大之处在于无需复杂的处理就可以将对象和一个二进制流之间进行互相转化。这一功能可以巧妙地被我们用于对象克隆,特别是当某些对象不提供Assign方法实现时。


6、Delphi对象的深克隆
前 面我讲过,当克隆一个对象时,如果克隆的是该对象所有的状态,即被克隆对象的所有变量都含有与原始对象相同的值,称为完全克隆。否则,有选择地克隆原始对 象的部分状态,只能称为不完全克隆。这是从对象克隆的广度上看问题。如果从对象克隆的深度上看,对象克隆还可以分成浅克隆和深克隆。所谓浅克隆仅仅克隆所 考虑的对象,而不深入克隆它所引用的对象。如果在克隆当前对象时,同时也克隆了该对象所引用的对象,这就是所谓的深克隆了。在浅克隆中,被克隆的对象所引 用的对象仍然是原始对象所引用的那个对象;而深克隆中,被克隆的对象所引用的对象已经是一个新的对象。这就是说,深克隆同时也克隆了引用对象本身,而不是 它的一个引用。由于深克隆间接复制了引用的对象,如果出现循坏引用,可能会出现意想不到的问题。所以当一个类引用了不支持串行化的间接对象,或者引用含有 循环结构的时候,则不能考虑使用深克隆。
浅克隆和深克隆问题也称为浅复制和深复制(shallow copy versus deep copy)的问题,该问题的焦点在于是否复制实例变量,亦或只是共享该变量的引用。浅克隆是简单也容易达成的,但是复杂结构原型的克隆一般需要进行深复 制,因为这样可以保证原型对象与被克隆的对象彼此独立,互不影响。
虽然深克隆的问题比较复杂,但使用流化对象的技巧可以方便实现对象的深克隆。下面我给出一个演示性的例子。
下 图 是这个原型模式深克隆演示程序的类图。从图中看出,TStreamableClass包含了FContainedClass和FMemo两个数据成员变 量,分别引用了TContainedClass 和TMemoPrototype的实例对象。TStreamableClass的Clone方法实现了对象的深克隆。


示 例程序 9 7是PrototypeByStream单元程序的源码,这里实现了原型模式。需要注意的是,声明对象时,只有放置在published域的数据成员(或 属性)才能够被自动流化。也就是说,要克隆的引用对象变量要么直接放在published域(如TStreamableClass 的数据成员FMemo),要么将访问其的属性放在published域(如TStreamableClass 的属性ContainedClass)。
从流中读出对象时,需要将该对象转型为原来的类型,比如:

AClassInstance := 
TStreamableClass( ReadComponentResFile('DeepClone', nil));

但是事先要使用RegisterClass 或RegisterClasses注册自己的类,比如: 

RegisterClasses([TMemoPrototype,TContainedClass, TStreamableClass]);

否则会找不到类,出现EClassNotFound异常。

示例程序 9 7 PrototypeByStream单元程序源码
unit PrototypeByStream;

interface

uses
 SysUtils, Windows, Messages, Classes, Graphics, Controls,
 Forms, Dialogs,StdCtrls;

type

 TMemoPrototype = class (TMemo)
 public
   constructor create(AOwner:TComponent);override;
 published
   function Clone: TObject;
 end;

 TContainedClass = class(TPersistent)
 private
   FSomeData: Integer;
   procedure SetSomeData(Value: Integer);
 public
   constructor Create;
 published
property SomeData: Integer 
read FSomeData write SetSomeData;
 end;

 TStreamableClass = class(TComponent)
 private
   FContainedClass: TContainedClass;
 public
   constructor Create(AOwner: TComponent); override;
   destructor Destroy; override;
 published
   FMemo:TMemoPrototype;
   function Clone: TObject;
property ContainedClass: TContainedClass 
read FContainedClass write FContainedClass;
 end;

implementation

procedure TContainedClass.SetSomeData(Value: Integer);
begin
 FSomeData := Value;
end;

constructor TContainedClass.Create;
begin
 FSomeData := 42;
end;

constructor TStreamableClass.Create(AOwner: TComponent);
begin
 inherited Create(AOwner);
 FContainedClass := TContainedClass.Create;
 FMemo:=TMemoPrototype.create(AOwner);
end;

destructor TStreamableClass.Destroy;
begin
 FContainedClass.Free;
end;

function TStreamableClass.Clone: TObject;
var
 AClassInstance: TStreamableClass;
begin
 AClassInstance := TStreamableClass.Create(nil);
 WriteComponentResFile('DeepClone', AClassInstance);
 FreeAndNil(AClassInstance);
AClassInstance := 
TStreamableClass( ReadComponentResFile('DeepClone', nil));
 result:=AClassInstance;
end;

{TMemoPrototype}
function TMemoPrototype.Clone: TObject;
var
 tmp:TMemoPrototype;
 TmpStream:TStream;
begin
 WriteComponentResFile('MemoPrototype.dat',self);
 tmp:=TMemoPrototype(ReadComponentResFile('MemoPrototype.dat',nil));
 result:=tmp;
end;

constructor TMemoPrototype.create(AOwner:TComponent);
begin
 inherited;
 Width:=100;
 Height:=50;
 Left:=50;
 Top:=50;
 Font.Color:=clBlue;
end;

initialization
 RegisterClasses([TMemoPrototype,TContainedClass, TStreamableClass]);
finalization
end.


在示例程序 9 8中,我使用并演示了使用流技术克隆对象的原型模式。除了在演示窗体上可以看到克隆的对象,我们还可以测试被克隆的引用对象是一个新对象还是原始对象的一个引用。
-----------------
示例程序 9 8 演示窗体的程序源码
unit PrototypeStreamForm;

interface

uses
 Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
 Dialogs, StdCtrls,PrototypeByStream;

type
 TForm1 = class(TForm)
   btnClone: TButton;
   btnDeepClone: TButton;
   procedure FormCreate(Sender: TObject);
   procedure btnCloneClick(Sender: TObject);
   procedure btnDeepCloneClick(Sender: TObject);
 private
   FPrototype1:TMemoPrototype;
   FPrototype2:TStreamableClass;
   n:integer;
 public
   { Public declarations }
 end;

var
 Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
begin
 FPrototype1:=TMemoPrototype.create(self) ;
 n:=1;
end;

procedure TForm1.btnCloneClick(Sender: TObject);
var
 tmp:TMemoPrototype;
begin
 tmp:=TMemoPrototype(FPrototype1.clone);
 tmp.Name:='Memo'+IntToStr(n);
 tmp.Parent:=self;
 tmp.Lines.Add('克隆文本框之'+IntToStr(n));
 tmp.Left:=tmp.Left+30*n;
 tmp.Top:=tmp.Top+30*n;
 inc(n);
end;

procedure TForm1.btnDeepCloneClick(Sender: TObject);
var
 AInstance: TStreamableClass;
begin
 FPrototype2:=TStreamableClass.create(self);
 AInstance:=TStreamableClass(FPrototype2.Clone);
 showmessage(inttostr(AInstance.ContainedClass.SomeData));
 AInstance.ContainedClass.SomeData:=30;
 showmessage(inttostr(AInstance.ContainedClass.SomeData));
 AInstance.FMemo.Parent:=self;
AInstance.FMemo.Lines.Add('Deep Clone OK!');

 //测试被克隆的引用对象是一个新对象还是原始对象的一个引用。
 if (AInstance.FMemo=FPrototype2.FMemo) or
    (AInstance.ContainedClass=FPrototype2.ContainedClass)
 then
 begin
   showmessage('克隆的是对象引用!');
 end;

end;

end.

这里我介绍的重点是模式及其应用实践。而Delphi中关于流的技术和对象流化机制还相当复杂,有兴趣的读者可以进一步查阅有关书籍和资料。

已标记关键词 清除标记
相关推荐
EhLib.VCL 9.5内部版本9.5.012专业版。 ---------------------------------------------- 该库包含Borland Delphi版本7、9的组件和类。 Developer Studio 2006,Delphi 2007,Embarcadero RAD Studio 2009-XE10.3,Lazarus。 目录 ----------------- 总览 安装库 安装帮助 示范节目 注册和价格 其他资讯 关于作者 从哪儿开始。 ------------------- 从主要的Demo项目开始概述库 。\ Demos \ Bin \ MainDemo.Exe。 (已编译的演示文件在该库的评估版中可用) 如果您使用了库的早期版本,则可以阅读摘要 的新功能和更改中的内容history-eng.html。 此版本库中有关新功能的更多详细信息 可以在文件中找到-关于EhLib 9.5 Eng.doc 要在IDE中安装该库的新版本,请使用安装程序 。\ Installer \ EhLibInstaller.exe 如果安装时有任何问题,请写信给ehlib支持 地址support@ehlib.com 您还可以按照以下说明在库IDE中手动安装文件 第2章安装库 安装后,请确保所有已安装组件的可操作性。 为此,请打开IDE,编译并启动一个主要的演示项目。 。\ Demos \ MainDemo \ Project1_XE2.dpr 阅读下一个文件,以获取有关使用库组件的完整说明: 。\ Hlp \ ENG \“ EhLib-用户指南.doc” 在文件中阅读有关EhLib for Lazarus的信息-Lazarus <*> \ readme.txt 总览 -------- 该库包含几个组件和对象。 TDBGridEh组件 TDBGridEh提供TDBGrid的所有功能 并添加了一些新功能,如下所示: 允许选择记录,列和矩形区域。 可以对应于多个/所有列的特殊标题。 能够显示总和/计数/其他字段值的页脚。 自动调整列大小以将网格宽度设置为等于客户端宽度。 能够更改行和标题的高度。 允许自动中断单行长标题和数据行 到多行。 标题可以充当按钮,并可以选择显示排序标记。 自动排序标记。 能够用省略号截断长文本。 查找列表可以显示几个字段。 在查找字段中进行增量搜索。 冻结的列。 DateTime选择器支持TDateField和TDateTimeField。 允许根据字段值显示TImageList中的位图。 允许隐藏和跟踪水平或垂直滚动​​条。 允许隐藏列。 允许显示冻结,页脚和数据行的3D框架。 允许绘制备忘字段。 多行就地编辑器。 比例滚动与数据集的排序无关。 自动显示布尔字段的复选框。允许显示 其他类型字段的复选框。 具有保存和还原布局的程序(可见列,列 注册表或ini文件中的顺序,列宽,排序标记,行高)。 允许显示提示(工具提示)以显示单元格中不适合的文本。 允许将数据导出为Text,Csv,HTML,RTF,XLS和内部格式。 允许从文本和内部格式导入数据。 可以对各种数据集中的数据进行排序。 可以过滤各种数据集中的数据。 当DBGridEh连接到TMemTable类型的数据集时,它允许: 在不移动活动记录的情况下查看所有数据。 显示TMemTable记录的树型结构。 在SubTitle过滤器的下拉列表中自动形成值列表。 根据所选库伦创建分组记录。 TDBVertGridEh组件 在“垂直方向”中显示数据集中的一条记录的组件。 有一个特殊的列来显示字段标题 可以像DBGridEh中一样自定义就地编辑器和单元格数据。 TDBLookupComboboxEh组件 提供TDBLookupCombobox的所有功能并添加 几个新功能如下: 可以有平面样式。 允许只将值分配给KeyValue属性 显示文本属性。 允许将值键入(分配)到数据列表中未包含的Text属性 (样式= csDropDownEh)。 允许将KeyValue和Text保持为互不影响的值。 当KeyField,ListField,ListSource,DataField和DataSource生效 属性为空。 下拉列表可以:
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页