请教:关于各个单元的 finalization 顺序问题 ( 积分: 200 )

  • 主题发起人 主题发起人 fishjam
  • 开始时间 开始时间
F

fishjam

Unregistered / Unconfirmed
GUEST, unregistred user!
在我们开发的系统中,利用接口来实现插件和主程序各个功能模块之间的通信(Exe+Bpl+Dll方法)。于是出现比较奇怪的问题:当程序在结束时,有些窗体的 Destory 中会调用一些接口的方法进行处理,但是这时这些接口已经被释放了(在 finalization 中由Delphi开发环境自动赋nil值释放)。
我的问题就是:form的 Destroy 方法怎么会在 finalization 之后才执行,而各个单元的finalization 顺序又是怎么决定的。
由于代码量比较大,我写了如下的测试代码进行问题测试(不过没有试出Destroy在 finalization 之后执行的情况)。
我的开发环境是Delphi7+Win2KSP4

工程文件:(创建一个两个窗体,一个附加单元的程序)
program Project1;
uses
Forms,
Unit1 in 'Unit1.pas' {Form1},
Unit2 in 'Unit2.pas' ,
Unit3 in 'Unit3.pas' {Form3};
{$R *.res}
begin
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.CreateForm(TForm3, Form3);
Application.Run;
end.

窗体1:(程序的主窗体,仅仅用于显示)
unit Unit1;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, CSIntf, StdCtrls;
type
TForm1 = class(TForm)
btn1: TButton;
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
end.

单元2(定义接口声明和实现)
unit Unit2;

interface
uses
Classes,Windows,SysUtils;

type
IMyInterface= interface(IUnknown)
procedure Func1;
procedure Func2;
end;

TMyInterfaceObj= class(TInterfacedObject,IMyInterface)
public
FNum:Integer;
procedure Func1;
procedure Func2;
constructor Create;
destructor Destroy;override;
end;
var
Num:Integer;
G_MyInteface:IMyInterface;

implementation
//请注意,如果我把这里的包含Unit3的代码删除,程序就不会出错了
uses
unit3;
constructor TMyInterfaceObj.Create;
begin
inherited Create;
FNum:=Num;
Inc(Num);
OutputDebugString(PChar('TMyInterfaceObj.Create'+IntToStr(FNum)));
end;

destructor TMyInterfaceObj.Destroy;
begin
OutputDebugString(PChar('TMyInterfaceObj.Destroy'+IntToStr(FNum)));
inherited;
end;

procedure TMyInterfaceObj.Func1;
begin
OutputDebugString('TMyInterfaceObj.func1');
end;

procedure TMyInterfaceObj.Func2;
begin
OutputDebugString('TMyInterfaceObj.func2');
end;

initialization
Num:=1;
G_MyInteface:=TMyInterfaceObj.Create
//此处创建全局接口变量
finalization
G_MyInteface:=nil
//此处释放接口变量(如果没有这个语句,Delphi编译器也会自动加上,加上以后可以看出程序流程)
end.

窗体单元3,此处调用全局的接口变量
unit unit3;

interface

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

type
TForm3 = class(TForm)
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
end;

var
Form3: TForm3;

implementation

{$R *.dfm}

procedure TForm3.FormCreate(Sender: TObject);
begin
G_MyInteface.Func1;
end;
procedure TForm3.FormDestroy(Sender: TObject);
begin
G_MyInteface.Func2
//在我们的实际代码中,这个地方会出现访问零地址的错误,不过我的测试代码没有写出这种情况
end;
initialization
G_MyInteface.Func2
//如果在unit2中加入了对unit3的单元引用,这里就会报零地址错误
finalization
G_MyInteface.Func1;
end.
 
在我们开发的系统中,利用接口来实现插件和主程序各个功能模块之间的通信(Exe+Bpl+Dll方法)。于是出现比较奇怪的问题:当程序在结束时,有些窗体的 Destory 中会调用一些接口的方法进行处理,但是这时这些接口已经被释放了(在 finalization 中由Delphi开发环境自动赋nil值释放)。
我的问题就是:form的 Destroy 方法怎么会在 finalization 之后才执行,而各个单元的finalization 顺序又是怎么决定的。
由于代码量比较大,我写了如下的测试代码进行问题测试(不过没有试出Destroy在 finalization 之后执行的情况)。
我的开发环境是Delphi7+Win2KSP4

工程文件:(创建一个两个窗体,一个附加单元的程序)
program Project1;
uses
Forms,
Unit1 in 'Unit1.pas' {Form1},
Unit2 in 'Unit2.pas' ,
Unit3 in 'Unit3.pas' {Form3};
{$R *.res}
begin
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.CreateForm(TForm3, Form3);
Application.Run;
end.

窗体1:(程序的主窗体,仅仅用于显示)
unit Unit1;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, CSIntf, StdCtrls;
type
TForm1 = class(TForm)
btn1: TButton;
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
end.

单元2(定义接口声明和实现)
unit Unit2;

interface
uses
Classes,Windows,SysUtils;

type
IMyInterface= interface(IUnknown)
procedure Func1;
procedure Func2;
end;

TMyInterfaceObj= class(TInterfacedObject,IMyInterface)
public
FNum:Integer;
procedure Func1;
procedure Func2;
constructor Create;
destructor Destroy;override;
end;
var
Num:Integer;
G_MyInteface:IMyInterface;

implementation
//请注意,如果我把这里的包含Unit3的代码删除,程序就不会出错了
uses
unit3;
constructor TMyInterfaceObj.Create;
begin
inherited Create;
FNum:=Num;
Inc(Num);
OutputDebugString(PChar('TMyInterfaceObj.Create'+IntToStr(FNum)));
end;

destructor TMyInterfaceObj.Destroy;
begin
OutputDebugString(PChar('TMyInterfaceObj.Destroy'+IntToStr(FNum)));
inherited;
end;

procedure TMyInterfaceObj.Func1;
begin
OutputDebugString('TMyInterfaceObj.func1');
end;

procedure TMyInterfaceObj.Func2;
begin
OutputDebugString('TMyInterfaceObj.func2');
end;

initialization
Num:=1;
G_MyInteface:=TMyInterfaceObj.Create
//此处创建全局接口变量
finalization
G_MyInteface:=nil
//此处释放接口变量(如果没有这个语句,Delphi编译器也会自动加上,加上以后可以看出程序流程)
end.

窗体单元3,此处调用全局的接口变量
unit unit3;

interface

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

type
TForm3 = class(TForm)
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
end;

var
Form3: TForm3;

implementation

{$R *.dfm}

procedure TForm3.FormCreate(Sender: TObject);
begin
G_MyInteface.Func1;
end;
procedure TForm3.FormDestroy(Sender: TObject);
begin
G_MyInteface.Func2
//在我们的实际代码中,这个地方会出现访问零地址的错误,不过我的测试代码没有写出这种情况
end;
initialization
G_MyInteface.Func2
//如果在unit2中加入了对unit3的单元引用,这里就会报零地址错误
finalization
G_MyInteface.Func1;
end.
 
按uses中引用的次序初始化,finalization 顺序正好相反
 
//来自:thx1180, 时间:2005-5-27 19:33:26, ID:3086602
//按uses中引用的次序初始化,finalization 顺序正好相反
这是正解.

//procedure TForm3.FormDestroy(Sender: TObject);
//begin G_MyInteface.Func2
end;
你可以在实际代码中在TMyInterfaceObj.Destroy添加断点,跟踪判断是那里导致的接口释放.

//implementation
//请注意,如果我把这里的包含Unit3的代码删除,程序就不会出错了
//uses unit3;
这种可能涉及到生命周期的单元引用,你最好把它们放到interface节的uses里边.这两个地方是不一样的
 
刚发现在工程文件中的uses顺序也会影响到程序是否能正常运行.如果把unit3放置在unit2的前面,则不论 Unit2 中是否有那个多余的 Use Unit3 语句,程序都不会出错了.
我尝试着分析了一下程序中的初始化顺序,各位看看对不对.

首先工程文件中的包含顺序是 Unit1,Unit2,Unit3 的话,程序首先分析Unit1单元,由于本单元中没有包含其他的单元文件,因此先初始化本单元(在Unit1中加入initialization就可以看到效果).
然后在依次分析工程文件中Unit2文件,由于在其 implementation 中包含了Unit3,因此系统此时先分析Unit3文件,发觉又反向包含了Unit2文件(此处的Uses为必须),由于Unit2已经在系统的分析列表中了,此时系统就执行Unit3的初始化代码,由于这个时候Unit2的初始化还没有执行,G_MyInteface 为nil,程序就会出错了.
当我们将Unit2 中多余的对 Unit3 的包含删除掉,系统分析到Unit2时就不会先分析执行 Unit3中的初始化代码,因此程序不会出错.

当将工程文件中的包含顺序改为 Unit1,Unit3,Unit2 时,当系统开始分析Unit3的代码时,发现其中对Unit2有包含,于是分析Unit2的代码,此时即使其中包含了Unit3,由于Unit3已经在分析列表中了,系统执行Unit2的初始化代码,创建 G_MyInteface 的全局变量,以后程序的访问就不会出错了.

总结起来就是一句话:Delphi的程序从工程文件中的包含文件列表开始分析,依次展开各个单元文件的包含列表,先执行不再包含其他单元文件或所包含的单元文件已经在系统已分析单元文件堆栈中的单元初始化代码(有些拗口哈.^_^)

由于这种对文件包含顺序或对无用单元文件的包含产生错误的情况很少(估计只有在使用接口编程的情况下会出现),可能对各位帮助不是很大,只是自己发现了,拿出来抛砖引玉罢了.

另外发现对单元文件的包含,无论是放在 interface 处还是 implementation 处,对本程序不造成任何影响.我只知道两个地方的不同就是 防止循环引用的错误,不知道是不是还有其他用处 ,请知道的人告诉一下.

如果没有新的说法我就准备结分了.
 
和你引用的顺序有关
 
BTW:有的时候我们通过Delphi的调试器单步跟踪时,发现某行代码执行了数遍,但是通过代码中的跟踪日志来看只执行了一遍,不知道各位是否遇到过这种情况.是Delphi的bug还是怎么,有没有解决方法.

我们开发的是 Exe+Bpl+Dll的模式
 

Similar threads

S
回复
0
查看
3K
SUNSTONE的Delphi笔记
S
S
回复
0
查看
2K
SUNSTONE的Delphi笔记
S
I
回复
0
查看
720
import
I
后退
顶部