如何安全地在Delphi中创建和释放多个对象

13

如何安全地创建和释放多个对象?

基本上,就是这样:

  newOrderSource := TWebNewOrderSource.Create();
  twData := TTWData.Create();
  webData := TWebData.Create();

  try
    //do stuff
  finally
    newOrderSource.Free();
    twData.Free();
    webData.Free();
  end;

在这种情况下,第二个和第三个创建命令是不安全的,因为它们涉及到数据库操作。我应该将所有的创建操作都放在try块中,并在调用free之前检查它们是否已被分配吗?

2个回答

18

如果你首先将变量赋值为nil,那么你可以使用一个try块来实现这一点,例如:

newOrderSource := nil;
twData := nil;
webData := nil;
try
  newOrderSource := TWebNewOrderSource.Create();    
  twData := TTWData.Create();    
  webData := TWebData.Create();    

  //do stuff    
finally    
  webData.Free();    
  twData.Free();    
  newOrderSource.Free();    
end;    

这个可以工作的原因是因为 Free() 在释放内存前会检查 Self 是否为 nil


我在发布一个非常类似的答案,一个小变化是将newOrderSource := TWebNewOrderSource.Create();放在try之前,并丢弃newOrderSource := nil; - Gerry Coll
2
这也可以,并且去掉了一个不必要的赋值,但在这种情况下,我更倾向于简单和一致性,因为如果“do stuff”调用数据库,保存赋值很可能不会有太大的区别。 - chuckj
6
我经常使用这个,但是就安全而言,你应该注意,与OP的原始代码相比,这可以保护你免受任何一个构造函数引发的异常,但无法保护你免受在释放对象时引发的异常。如果你对此非常担心,你唯一的选择是有多少对象就要有多少个try/finally(或让它们实现接口并让它们超出范围)。 - Lieven Keersmaekers
1
@Leonardo 如果在构造函数中引发异常,则会运行析构函数,以便可以清理部分构造对象。当这种情况发生时,任何未创建的对象成员都将为 nil,因为 Delphi 将所有内存初始化为零,这恰好与 nil 相同。这就是为什么您不需要使用 if Assigned() 保护每个 Free 调用的原因。这也是为什么要调用 Free 而不是 Destroy 的原因。 - David Heffernan
1
@Leonardo Free不是一个虚方法,因此编译器会生成一个普通的函数调用,并将Self作为参数传递。这就像将nil作为接受TObject的过程的第一个参数一样,只是看起来有点奇怪。 - chuckj
显示剩余7条评论

13

如大家所知,管理对象的标准方法如下:

A := TMyObject.Create;
try
  A.DoSomething;
finally
  A.Free;
end;

如果在TMyObject.Create中出现异常,则会调用析构函数,然后抛出异常。在这种情况下,A将不被分配。

当您有多个对象时,可以重复此模式:

A := TMyObject.Create;
try
  B := TMyObject.Create;
  try
    A.DoSomething;
    B.DoSomething;
  finally
    B.Free;
  end;
finally
  A.Free;
end;

这很快就会变得一团糟,因此才有了这个问题。

一个常见的技巧是利用这样一个事实:Free 可以安全地在 nil 对象引用上调用。

A := nil;
B := nil;
try
  A := TMyObject.Create;
  B := TMyObject.Create;
  A.DoSomething;
  B.DoSomething;
finally
  B.Free;
  A.Free;
end;

这种方式的小缺陷是,它对于在B.Free中抛出异常不具有弹性,但是将其视为可以忽略的失败条件并非不合理。析构函数不应该引发异常。如果他们这样做了, 那么您的系统可能无法挽回地损坏。

随着添加更多对象,上述模式可能会变得有点混乱,因此我个人使用以下辅助方法。

procedure InitialiseNil(var Obj1); overload;
procedure InitialiseNil(var Obj1, Obj2); overload;
procedure InitialiseNil(var Obj1, Obj2, Obj3); overload;

procedure FreeAndNil(var Obj1); overload;
procedure FreeAndNil(var Obj1, Obj2); overload;
procedure FreeAndNil(var Obj1, Obj2, Obj3); overload;

实际上,我的代码有更多参数的版本。为了方便维护,这段代码全都是从一个简短的Python脚本自动生成的。

这些方法的实现方式很显然,例如:

procedure FreeAndNil(var Obj1, Obj2);
var
  Temp1, Temp2: TObject;
begin
  Temp1 := TObject(Obj1);
  Temp2 := TObject(Obj2);
  Pointer(Obj1) := nil;
  Pointer(Obj2) := nil;
  Temp1.Free;
  Temp2.Free;
end;

这使我们可以像这样重写上面的示例:

InitialiseNil(A, B);
try
  A := TMyObject.Create;
  B := TMyObject.Create;
  A.DoSomething;
  B.DoSomething;
finally
  FreeAndNil(B, A);
end;

你不能让你的辅助方法接收一个值数组,这样你就可以将任意数量的对象传递给它们吗? - Jerry Gagnon
@Jerry 这也可以,但我更倾向于此时不使用开放数组语法。我的理由是这些函数只编写一次,从不修改。因此,我对重复性的关注程度比在被修改的实际代码中要低。动机是使调用代码更清晰,而这是被修改的代码。 - David Heffernan

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