TPersistent与对象赋值

TPersistent与对象赋值
在Object Pascal中,所有的简单类型(或称编译器内置类型,即非“类”类型,如Integer、Cardinal、Char、Record等类型)的赋值操作所进行的都是位复制,即将一个变量所在的内存空间的二进制位值复制到被赋值的变量所载的内存空间中。
如定义这样一个记录类型:
type
TExampleRec = record
Member1 : Integer;
Member2 : Char;
end;
在代码中,声明例如两个TExampleRec类型的变量实体,并在它们之间进行赋值:
var
A, B : TExampleRec;
begin
A.Member1 := 1;
A.Member2 := 'A';
B := A;
end;
其中,B := A;的结果将导致A的所有值都被复制到B中,A和B各自拥有一份它们的值。查看这段代码的编译结果:
mov [esp], $00000001 // A.Member1 := 1;
mov byte ptr [esp + $04], $41 // A.Member2 := ′A′;
mov eax, [esp] // B.Member1 := A.Member1
mov [esp + $08], eax
mov eax, [esp + $04] // B.Member2 := A.Member2
mov [esp + $0c], eax
就可以非常清楚地看到:
B := A;

B.Member1 := A.Member1;
B.Member2 := A.Member2;
是等价的。
对于简单类型,可以简单地以变量名称来进行赋值,那么对于所谓的复杂类型——“类”类型呢?
此前曾经提到过,Delphi向Object Pascal引入了所谓的“引用/值”模型,即对于简单类型的变量,采用“值”模型,它们在程序中的传递方式全部是基于“值”进行的。而复杂类型的变量,即类的实例对象,采用“引用”模型,因此在程序中所有类的对象的传递,全部基于其“引用”,也就是对象的指针。
如果将两个对象通过名称直接进行简单的赋值,将导致对象指针的转移,而并非复制它们之间的内存空间的二进制值。例如,将上述的TExampleRec改成Class类型:
type
TExample = class
public
Member1 : Integer;
Member2 : Char;
end;
并将赋值的代码改为:
var
A, B : TExample;
begin
A := TExample.Create();
B := TExample.Create();
ShowMessage(IntToStr(Integer(A))); // 输出13513320
ShowMessage(IntToStr(Integer(B))); // 输出 13513336
A.Member1 := 1;
A.Member2 := 'A';
B := A;
ShowMessage(IntToStr(Integer(B))); // 输出 13513320
......
这段代码中的3个ShowMessage调用,将输出对象所在内存空间的地址值。可以很明显看到,第3个ShowMessage输出的B对象所在的内存地址已经指向了A对象所在内存地址。此时,B和A所使用的数据将是同一份数据,若修改A的Member1的值,那么B的Member1也将同时被修改。而原先B所在的空间(13513336)已经失去了引用它的指针,于是就造成了所谓的“内存泄漏”。如图
可见,简单、直接地通过对象名称进行赋值是达不到复制对象的目的的。如果的确需要复制一个对象,那么难道真的要如同
B.Member1 := A.Member1;
B.Member2 := A.Member2;
这样来进行吗?即使可以这样做,那private数据如何复制呢?
可以为类增加一个Assign方法,以进行对象间的复制。例如修改以上的TExample类:
type
TExample = class
Member1 : Integer;
Member2 : Char;
public
procedure Assign(Src : TExample);
end;

实现该类的Assign方法如下:
procedure TExample.Assign(Src: TExample);
begin
Member1 := Src.Member1;
Member2 := Src.Member2;
end;
如此便可以进行TExample类实例对象间的复制:
var
A, B : TExample;
begin
A := TExample.Create();
B := TExample.Create();
A.Member1 := 1;
A.Member2 := 'A';
B.Assign(A);
......
如此庞大的VCL库中,肯定需要提供这样一种机制来保证对象间的有效赋值,于是VCL提供了一个抽象类——TPersistent。
TPersistent为对象间的复制式赋值定义了一套接口规范:
TPersistent = class(TObject)
private
procedure AssignError(Source: TPersistent);
protected
procedure AssignTo(Dest: TPersistent); virtual;
procedure DefineProperties(Filer: TFiler); virtual;
function GetOwner: TPersistent; dynamic;
public
destructor Destroy; override;
procedure Assign(Source: TPersistent); virtual;
function GetNamePath: string; dynamic;
end;
在TPersistent的声明中,有两个Public的方法(Destroy在此不讨论),其中GetNamePath是Delphi的集成开发环境内部使用的,VCL不推荐直接对它的调用。而Assign方法则是为完成对象复制而存在的,并且被声明为虚方法,以允许每个派生类定义自己的复制对象的方法。
如果正在设计的类需要有这种允许对象复制的能力,则让类从TPersistent派生并重写Assign方法。
如果没有重写Assign方法,则TPersistent的Assign方法会将复制动作交给源对象来 进行:
procedure TPersistent.Assign(Source: TPersistent);
begin
if Source <> nil then
Source.AssignTo(Self) // 调用源对象的AssignTo方法
else
AssignError(nil);
end;
可以在TPersistent类的声明的protected节中找到AssignTo方法的声明,它也是一个虚方法。
如果将复制动作交给源对象来完成,那么必须保证源对象的类已经重写了AssignTo方法,否则将抛出一个“Assign Error”异常:
procedure TPersistent.AssignTo(Dest: TPersistent);
begin
Dest.AssignError(Self);
end;
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;
AssignError是一个private方法,仅仅用于抛出赋值错误的异常。
在TPersistent的声明中,GetOwner方法是被前面所述由Delphi内部使用的GetNamePath所调用。
最后还剩下一个虚方法DefineProperties(),它则是为TPersistent的另一个使命而存在:

对象持久。一个对象要持久存在,就必须将它流化(Streaming),保存到一个磁盘文件(.dfm文件)中。TPersistent也使得其派生类具有这种能力,但它作为抽象类只是定义接口而并没有给出实现。可以看到,DefineProperties是一个空的虚方法:
procedure TPersistent.DefineProperties(Filer: TFiler);
begin
end;
这留待其派生类来实现。
对于对象持久的实现类,最典型的就是TComponent,每个组件都具有保存自己的能力。因此下面将以TComponent来说明对象持久的实现,虽然它是在TPersistent中定义接口的。

0 评论:

发表评论

最近访问者

Google

存档

Labels