Delphi中的单元和终结

5

我有两个单元unitA和unitB。 类TFoo在unitB中声明。

在unitA的终结部分中调用B.Free总是安全的吗?

这取决于unitA和unitB在dpr文件中的顺序吗?

当执行unitA终结时,我能确定unitB存在吗?

unit unitB;
interface
type
 TFoo = class
   // code...
  end;
 // code....
end;

unit unitA;
// code..
implementation
uses
 unitB;
var
 A: TStringList;
 B: UnitB.TFoo;

initialization
 A:= TStringList.Create;
 B:= UnitB.TFoo.Create;
finalization
 A.Free;
 B.Free;  // Is it safe to call?
end.
6个回答

17

是的,你应该没问题,因为B是在Unit A中创建的。规则是Initialization部分按照它们在DPR中的顺序调用,除非其中一个单元引用了另一个单元。在这种情况下,被引用的单元先被初始化。Finalization则以相反的顺序进行。

在你的情况下,Unit B没有初始化部分,因此这是无关紧要的。但是,在执行Unit A的初始化部分时,它将使用Unit B中的TFoo定义。

有关Initialization和Finalization部分的另一个警告是 - 它们发生在全局异常处理程序之外。发生在这里的任何异常都会终止应用程序。因此,在大型程序中跟踪和调试这些异常可能会很麻烦。你可以考虑在这里使用自己的异常日志记录,以确保安全。


请看我的注释。德里不能保证顺序。你的异常注释是正确的。 - DiGi
值得注意的是,Madexcept捕获了在终结部分引发的那些异常。 - utku_karatas

5
不行。你可以尝试,你可以希望,但在调用初始化和终结顺序上没有任何保证。请参见qc72245qc56034更多信息更新:
  1. 终结部分的执行顺序与初始化相反。您的示例是安全的,您不需要在单元之间调用初始化部分依赖关系
  2. Delphi可以混合调用单元(点1仍然有效,初始化和终结部分都会交换)

示例:

unitA // no dependency on unitB
var SomeController;
initialization
  SomeController := TSomeController.Create;
finalization
  SomeController.Free;

unitB
uses
  unitA;
initialization
  SomeController.AddComponent(UnitBClass);
finalization
  SomeController.RemoveComponent(UnitBClass);

常见的(正确的)调用顺序(99.99%)如下:

  1. unitA.initialization
  2. unitB.initialization
  3. 运行...
  4. unitB.finalization
  5. unitA.finalization

但有时候Delphi编译文件会出错:

  1. unitB.initialization - 这里出现AV错误
  2. unitA.initialization
  3. 运行...
  4. unitA.finalization
  5. unitB.finalization - 这里也会出错

小离题的故事:

我们有一个相当大的项目,Unit1中有Type1,Unit2中有Type2 = class(Type1)。文件在project.dpr中排序,在多年和添加Unit200(与unit1/2无依赖关系)之后,Delphi开始在Unit1.Initialization之前编译Unit2.Initialization。唯一安全的解决方案是从初始化部分调用自己的Init函数。


原问题并不涉及初始化/终止顺序。 - kludg
即使单元B先完成,也不会导致该单元停止存在。使用在单元B中定义的内容的代码将继续工作。 - Rob Kennedy
2
但这取决于情况。finalization 部分的执行顺序与初始化相反,但 Delphi 可以交换单元和顺序。 - DiGi
@kludg - 但这些信息太好了,不能被删除。干得好 DiGi。 - Gabriel
我喜欢你的单元名称 :) - Gabriel

3
在你展示的特定情况下,一切都会没问题。但只需稍作重构即可开始出现问题。
Delphi在确保单元仅在需要时保留在内存中方面做得很好。但前提是它知道需要哪个单元。
我经典的例子是一个仅包含对象列表的单元。
unit Unit1;

interface
uses
  Contnrs;
var
  FList : TObjectList;
implementation

initialization
  FList := TObjectList.Create(True);
finalization
  FList.Free;
end.

Unit1只显式依赖于Contnrs。Delphi只会确保Contnrs单元(以及可能的“子依赖”单元,但我不确定)仍在内存中加载。如果在列表中添加了一个TForm,则Forms单元可能已经在FList.free被调用时被终止,当它尝试释放包含的TForm时,程序将崩溃。Delphi无法知道Unit1需要Forms单元。在这种情况下,它将取决于dpr中声明单元的顺序。


3
据我所知,您的内容应该是完全有效的,有点笨拙但仍然有效。但更好的方法可能是在B单元中声明一个变量,并让B初始化/完成它。由于初始化发生在调用任何其他代码之前,只要它在A单元的使用子句中声明,它就会在向A单元提供之前进行初始化。
您可能还想考虑的另一步是将B的单元变量进一步作为按需加载的函数调用,但这也可能取决于您的使用情况。
例如:
unit unitB;
interface
type
 TFoo = class
   // code...
  end;
 // code....
 function UnitVarB:TFoo;

implementation

var
  gUnitVarB : TFoo;  

function UnitVarB:TFoo 
begin
  if not assigned(gUnitVarB) then
    gUnitVarB := TFoo.Create;

  result := gUnitVarB;
end;

finalization
  if assigned(gUnitVarB) then
    gUnitVarB.free;  //or FreeAndNil(gUnitVarB);

end;

unit unitA;

// code..
implementation

uses
 unitB;

var
 A: TStringList;

//code...
  ...UnitVarB....
//code...

initialization
 A:= TStringList.Create;
finalization
 A.Free;
end.

我记得在某个地方看到过,单元初始化可能会很昂贵,因为如果在编译期间您的uses子句中仍然引用了您不再直接引用的单元,则智能链接器将不会移除它,因为有初始化部分。虽然这听起来可能并不那么糟糕,但如果每个单元都有一个初始化部分,那么大多数Delphi程序将比它们现在已经有的要大得多
我并不是说不要使用它们,但我的经验法则是谨慎使用。
你的初始代码示例违反了这个规则。我想提一下。
Ryan

谢谢。函数UnitVarB是避免全局变量的聪明方法。 - pKarelian
我使用CnPack的Uses清理工具,它似乎跳过了具有初始化的单元。 - pKarelian

1

是的,这是安全的。您可以在dpr文件中先声明UnitB,以简化编译器的工作,但无论如何编译器都会解析引用。


1

为了充分披露,我自2005年以来就没有使用Delphi进行开发了。然而,我从1996年的Delphi 1开始专门使用Delphi进行开发,并在2001年获得了Delphi 5认证。话虽如此,我的finalization部分的使用很少。我只有在需要在.dpr中设置特殊内容时才会使用它。通常只有在我进行自定义组件开发并且需要使用其他自定义组件来管理某些依赖关系时才会发生这种情况。

对于典型的应用程序开发,我避免使用initialization/finalization部分,而是使用单例、外观和工厂等设计模式来管理我的类的创建和管理。内置垃圾收集器足以满足98.5%的项目需求。

回答你的问题,你需要在UnitA代码中设置对TFoo的依赖关系,并像Ryan建议的那样,在销毁之前确保它被分配。话虽如此,我鼓励你在投入太多时间之前确保使用initialization/finalization部分是必要的。


1
Delphi没有垃圾回收器。 - Jim McKeeth
2
海报最有可能是在提到VCL的“Owner”行为和/或者没有显式释放对象内存时,当进程终止时这些内存会被返回给系统,尽管它们的析构函数并没有运行。 - Deltics
1
Jim:谢谢你提醒我...显然,驱魔是完全成功的。 :o) Deltics:我记得我经常使用FreeAndNil来释放我在代码中创建的对象的内存和指针,但除非我正在使用系统管理的资源(如COM对象),否则我不会使用初始化部分。即使这样,它通常与依赖库相关或确保在我的组件启动时资源可用。 - Neil T.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接