避免嵌套的 try...finally 块在 Delphi

Posted

技术标签:

【中文标题】避免嵌套的 try...finally 块在 Delphi【英文标题】:Avoiding nested try...finally blocks in Delphi 【发布时间】:2013-09-01 01:39:01 【问题描述】:

今天早上我有这样一个想法,即避免嵌套 try finally 块,如下所示

procedure DoSomething;
var
  T1, T2, T3 : TTestObject;
begin
  T1 := TTestObject.Create('One');
  try
    T2 := TTestObject.Create('Two');
    try
      T3 := TTestObject.Create('Three');
      try
        //A bunch of code;
      finally
        T3.Free;
      end;
    finally
      T2.Free;
    end;
  finally
    T1.Free;
  end;
end;

利用接口的自动引用计数,我想出了

Type  
  IDoFinally = interface
    procedure DoFree(O : TObject);
  end;

  TDoFinally = class(TInterfacedObject, IDoFinally)
  private
    FreeObjectList : TObjectList;
  public
    procedure DoFree(O : TObject);
    constructor Create;
    destructor Destroy; override;
  end;

//...

procedure TDoFinally.DoFree(O : TObject);
begin
  FreeObjectList.Add(O);
end;

constructor TDoFinally.Create;
begin
  FreeObjectList := TObjectList.Create(True);
end;

destructor TDoFinally.Destroy;
begin
  FreeObjectList.Free;
  inherited;
end;

这样前面的代码块就变成了

procedure DoSomething;
var
  T1, T2, T3 : TTestObject;
  DoFinally : IDoFinally;
begin
  DoFinally := TDoFinally.Create;
  T1 := TTestObject.Create('One');
  DoFinally.DoFree(T1);
  T2 := TTestObject.Create('Two');
  DoFinally.DoFree(T2);
  T3 := TTestObject.Create('Three');
  DoFinally.DoFree(T3);
  // A Bunch of code;
end;

我的问题是:这行得通还是我忽略了什么?

在我看来,这看起来很酷,并且通过减少嵌套量使代码更易于阅读。它还可以扩展为存储匿名方法列表以运行以执行诸如关闭文件、查询等操作...

【问题讨论】:

我真的不喜欢这种方法。它实际上使代码 less 可读。当然,它最大限度地减少了您必须阅读的代码量,但您只会使事情复杂化。 编译器会放入一个隐藏的try-finally块,以确保接口递减(并释放)。 没错,@Nicholas。这有问题吗? @JerryDodge 我认为,每当您偏离“标准”Delphi 编码实践时,您都会使代码更难理解(对于除作者之外的任何人)——即使它更简单。请参阅 Rob Kennedy 使用 JCL 的答案 - 它更短更干净。我的目标更多是删除所有额外的东西,以便业务逻辑易于遵循。 另外不要忘记,如果内存出现问题,Add() 会引发异常,因此您必须使用try/except 块来确保未跟踪的对象仍然被释放。跨度> 【参考方案1】:

是的,它有效。

也许原始代码的嵌套 try-finally 块与使用引用计数对象来管理其他对象的生命周期的技术之间的唯一不同之处在于,如果在销毁任何对象时出现问题会发生什么。如果在销毁任何对象时出现异常,嵌套的 try-finally 块将确保任何剩余的对象仍将被释放。 TObjectList 中的 TDoFinally 不会这样做;如果列表中的任何项目无法销毁,则列表中的任何后续项目都将被泄露。

不过,实际上,这并不是一个真正的问题。任何析构函数都不应该抛出异常。如果确实如此,那么无论如何也没有任何方法可以从中恢复,因此是否有任何泄漏无关紧要。无论如何,您的程序应该会立即终止,因此有一个整洁的清理例程并不重要。

顺便说一句,JCL 已经提供了ISafeGuardIMultiSafeGuard 接口来管理本地对象的生命周期。例如,您可以像这样重写您的代码:

uses JclSysUtils;

procedure DoSomething;
var
  T1, T2, T3: TTestObject;
  G: IMultiSafeGuard;
begin
  T1 := TTestObject(Guard(TTestObject.Create('One'), G));
  T2 := TTestObject(Guard(TTestObject.Create('Two'), G));
  T3 := TTestObject(Guard(TTestObject.Create('Three'), G));
  // A Bunch of code;
end;

该库也不处理析构函数中的异常。

【讨论】:

在我继续玩的过程中,我已将 DoFree 更改为一个返回 TObject 的函数 - 这样就可以使用上面的一个衬里了。 +1 这个答案清楚地说明了如果析构函数引发异常,可能会出现原始代码的潜在问题。 +1 表示“任何析构函数都不应该抛出异常。如果确实如此,那么无论如何也没有任何方法可以从中恢复,因此是否有任何泄漏都没关系。” 【参考方案2】:

我通常会这样做,以在代码可读性和复杂性之间取得平衡:

procedure DoSomething;
var
  T1, T2, T3 : TTestObject;
begin
  T1 := nil;
  T2 := nil;
  T3 := nil;
  try
    T1 := TTestObject.Create('One');
    T2 := TTestObject.Create('Two');
    T3 := TTestObject.Create('Three');

    // A bunch of code

  finally
    T3.Free;
    T2.Free;
    T1.Free;
  end;
end;

警告:

这并不完全等同于你的原始代码,因为如果T3.Free 抛出异常,T2T1 将不会被释放并导致内存泄漏,对于T2.Free 中的相同尊重T1

但是,正如 Rob Kennedy 在他的评论中指出的那样,并在 his answer 中进行了更详细的解释,它等同于您使用 IDoFinally 的替代代码。

所以你的两种方法并不完全等价。

【讨论】:

是的,我使用过这种技术。如果你的析构函数抛出异常,那么你可能做错了什么。事实上,我在几年前制作的视频中介绍了这项技术-learndelphi.tv/index.php?option=com_content&id=58 相当于使用IDoFinally的代码。它不等同于带有嵌套 try-finally 块的代码。 如前所述,如果析构函数出现问题,此代码是不安全的。永远不要那样编码! @whosrdaddy 完全没有问题。当您描述的场景发生时,对象引用,例如T1,未分配。因此T1.Free 是无操作的。 @ArnaudBouchez 例如:TPdfArray.DestroyTPdfDictionaryElement.DestroyTPdfDictionary.DestroyTPdfStream.DestroyTPdfXref.Destroy 等等。更重要的是,任何时候你重写析构函数并且继承的析构函数调用Free,那么你就处于完全相同的情况。您的投诉不合理。【参考方案3】:

Smart pointers是实现自动内存管理的另一种方式。

ADUG 网站上有一篇 Delphi implementation 源自 Barry Kelly's 文章,介绍如何在 Delphi 中使用泛型、匿名方法实现强类型智能指针和接口:

    Smart pointers in Delphi Reference-counted pointers, revisited Somewhat more efficient smart pointers

您的代码将被重写为:

procedure DoSomething;
var
  T1, T2, T3 : ISmartPointer<TTestObject>;
begin
  T1 := TSmartPointer<TTestObject>.Create(TTestObject.Create('One'));
  T2 := TSmartPointer<TTestObject>.Create(TTestObject.Create('Two'));
  T3 := TSmartPointer<TTestObject>.Create(TTestObject.Create('Three'));

  // A bunch of code
end;

【讨论】:

+1 这就是我立刻想到的,但很快就找不到了。【参考方案4】:

我有一组辅助函数可以让@JRL 的方法更易于理解。

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(T1, T2, T3);
try
  T1 := TTestObject.Create('One');
  T2 := TTestObject.Create('Two');
  T3 := TTestObject.Create('Three');
finally
  FreeAndNil(T3, T2, T1);
end;

还有 Python 脚本:

count = 8


def VarList(count, prefix):
    s = ""
    for i in range(count):
        if i != 0:
            s = s + ", "
        s = s + prefix + str(i + 1)
    return s


def InitialiseNilIntf(count):
    print("procedure InitialiseNil(var " + VarList(count, "Obj") + "); overload;")


def FreeAndNilIntf(count):
    print("procedure FreeAndNil(var " + VarList(count, "Obj") + "); overload;")


def InitialiseNilImpl(count):
    print("procedure InitialiseNil(var " + VarList(count, "Obj") + ");")
    print("begin")
    for i in range(count):
        print("  Pointer(Obj%s) := nil;" % str(i + 1))
    print("end;")
    print()


def FreeAndNilImpl(count):
    print("procedure FreeAndNil(var " + VarList(count, "Obj") + ");")
    print("var")
    print("  " + VarList(count, "Temp") + ": TObject;")
    print("begin")
    for i in range(count):
        print("  Temp%s := TObject(Obj%s);" % (str(i + 1), str(i + 1)))
    for i in range(count):
        print("  Pointer(Obj%s) := nil;" % str(i + 1))
    for i in range(count):
        print("  Temp%s.Free;" % str(i + 1))
    print("end;")
    print()


for i in range(count):
    InitialiseNilIntf(i + 1)
print()
for i in range(count):
    FreeAndNilIntf(i + 1)
print()
for i in range(count):
    InitialiseNilImpl(i + 1)
print()
for i in range(count):
    FreeAndNilImpl(i + 1)

【讨论】:

你能把整个 Python 脚本输出贴在 pastebin 上吗?【参考方案5】:

我有时使用的替代方法:

procedure DoSomething;
var
  T1, T2, T3: TTestObject;
begin
  T1 := nil;
  T2 := nil;
  T3 := nil;
  try
    T1 := TTestObject.Create;
    T2 := TTestObject.Create;
    T3 := TTestObject.Create;
    // ...
  finally
    T1.Free;
    T2.Free;
    T3.Free;
  end;
end;

【讨论】:

与 JR 相同的代码,但没有解释或警告。如那里所述,如果析构函数出现问题,则此代码是不安全的。永远不要那样编码! @Arnaud 对析构函数中异常的关注是错误的。在每个人编写的代码中都是析构函数,其实现只是对 Free 的调用列表。没有人担心那里的问题。那么为什么要在这里紧张呢? 与@JRL 的解决方案相同的问题:.Create 调用应该在 try finally 块之前,因为在构造函数异常时会自动调用析构函数 @whosrdaddy 完全没有问题。当您描述的场景发生时,对象引用,例如T1,未分配。因此 T1.Free 是无操作的。 @DavidHeffernan 你在 VCL 中看到过这样的代码吗?嵌套 try..finally 块是正确/经典/安全的方式。此代码模仿编译器为本地接口变量发出的行为(将在方法进入时使用nil 进行初始化,并创建一个try..finally 块)。但是编译器生成的代码是安全的,而您很容易忘记手动编写一些东西,而 try..finally 很容易从 IDE 编辑器中插入,并且当您的代码增长并且您想用更小的方法重构它时更容易使用.【参考方案6】:

是的,此代码有效,尽管我个人倾向于将inherited 添加到您的构造函数和析构函数中。

有很多库都有使用这种机制的实现。用于移动平台的最新 Delphi 编译器使用 ARC(自动引用计数)管理对象生命周期,这是相同的技术,但融入了编译器对对象引用的处理。

【讨论】:

哎呀,你是对的 - 我已将继承添加到析构函数中。不知道在构造函数上是必需的吗? 你可以省略它,但你为什么要这样做呢? 有人告诉我,我们不会在 VCL 上看到 ARC,但我认为我们会在桌面上看到 FireMonkey - 这会很酷。【参考方案7】:

这是相同想法的稍微不同的实现:

unit ObjectGuard;

interface

type
  TObjectReference = ^TObject;

   TObjectGuard 
  TObjectGuard = class(TInterfacedObject)
  private
    fUsed: integer;
    fObjectVariable: array [0..9] of TObjectReference;
  public
    constructor Create(var v0); overload;
    constructor Create(var v0, v1); overload;
// add overloaded constructors for 3,4,5... variables
    destructor Destroy; override;
  end;

implementation

constructor TObjectGuard.Create(var v0);
begin
  fObjectVariable[0] := @TObject(v0);
  Tobject(v0) := nil;
  fUsed := 1;
end;

constructor TObjectGuard.Create(var v0, v1);
begin
  fObjectVariable[0] := @TObject(v0);
  Tobject(v0) := nil;
  fObjectVariable[1] := @TObject(v1);
  Tobject(v1) := nil;
  fUsed := 2;
end;

destructor TObjectGuard.Destroy;
var
  i: integer;
begin
  for i := 0 to FUsed - 1 do
    if Assigned(fObjectVariable[i]^) then
    begin
      fObjectVariable[i]^.Free;
      fObjectVariable[i]^ := nil;
    end;
  inherited;
end;

end.

优点是使用简单,如:

procedure Test;
var
  Guard: IInterface
  vStringList: TStringList;
  vForm: TForm;
begin
  Guard := TObjectGuard.Create(vStringList, vForm);
  vStringList := TStringList.Create;
  vForm:= TForm.Create(nil);
  // code that does something
end;

您可以在方法的开头创建 Guard 并在一次调用中传递任意数量的变量,这很方便。所以你不必先创建对象实例。

还要注意变量将在构造函数中自动初始化为 nil。

编辑: 此外,由于接口生命周期等于方法的执行时间,我们可以将其用于分析,也许 IFDEF ed 更容易控制。

【讨论】:

你能有一个构造函数来接受一个对象数组吗?这是一个有趣的解决方案,我将不得不使用。 我不认为你可以在这里以同样的方式使用数组。我只有 9 个重载的构造函数,但我在一个方法中从未真正拥有超过 4-5 个对象变量 我玩过这个并将其更改为使用 FreeList : TList;。并添加了一个 Fluent Add 方法 - function TObjectGuard.add(var v) : TObjectGuard;。这样您就可以做到- Guard := TObjectGuard.Create.Add(T1).Add(T2).Add(T3);【参考方案8】:

根据最终目的来理解使用哪种方法很棘手,但在某些情况下,我倾向于实现子例程,或者通常将我的代码分成不同的函数。比如……

FOne: TSomeObject;
FTwo: TSomeObject;
FThree: TSomeObject;

....

procedure DoSomething;
begin
  FOne:= TSomeObject.Create;
  try
    //a bunch of code which only needs FOne
    DoSomethingElse;
  finally
    FOne.Free;
  end;
end;

procedure DoSomethingElse;
begin
  FTwo:= TSomeObject.Create;
  try
    ShowMessage(DoYetAnother);
    //A bunch of code that requires FTwo
  finally
    FTwo.Free;
  end;
end;

function DoYetAnother: String;
begin
  FThree:= TSomeObject.Create;
  try
    //Do something with FOne, FTwo, and FThree
    Result:= FOne.Something + FTwo.Something + FThree.Something;
  finally
    FThree.Free;
  end;
end;

同样,如果没有更真实的场景,您将很难理解这将如何工作。我仍在考虑一个很好的例子,当我想到一个时会很乐意编辑。不过总体思路是将业务规则的不同部分分成不同的可重用代码块。

或者,您也可以将参数从一个过程传递到下一个过程,而不是声明全局变量。

【讨论】:

我喜欢这种观点(尽管我不再犹豫在 *** 上使用“喜欢”这个词 :-) 通常(当然,并非总是如此),如果一个人正在使用某个对象(创建,做某事,并且免费),这是由于某种原因而可以命名的。如果可以命名,我认为可以将其包装为方法(或过程)。 我通常倾向于同时使用不超过 2(两个!)交互对象。如果我必须使用第三个对象,我通常会将代码隔离到子程序(本地也很好)。该规则有效地将嵌套块的丑陋程度降低到可接受的最小值。【参考方案9】:

我认为没有必要将析构函数包装在接口中。默认情况下,Delphi 会在每个使用接口的过程/函数中构建一个幕后 try/finally,其中接口的引用计数会减少,从而在达到零时调用析构函数。

我进行了快速检查,但(至少在 Delphi 7 中)一个析构函数中的异常将停止其他析构函数,这很可悲。阻止这种情况的一种方法是在每个析构函数中编写 try/except,但这又是其他地方的更多代码,只是为了首先节省代码......

type
  IMyIntf=interface(IInterface)
    function GetName:string;
    procedure SetName(const Name:string);
    property Name:string read GetName write SetName;
  end;

  TMyObj=class(TInterfacedObject, IMyIntf)
  private
    FName:string;
    function GetName:string;
    procedure SetName(const Name:string);
  public
    constructor Create(const Name:string);
    destructor Destroy; override;
  end;

procedure TForm1.Button1Click(Sender: TObject);
var
  x,y:IMyIntf;
begin
  x:=TMyObj.Create('a');
  y:=TMyObj.Create('b');

  x.Name:='x';
  y.Name:='y';
end;

 TMyObj 

constructor TMyObj.Create(const Name: string);
begin
  inherited Create;
  FName:=Name;
end;

destructor TMyObj.Destroy;
begin
  MessageBox(Application.Handle,PChar(FName),'test',MB_OK);
  //test: raise Exception.Create('Destructor '+FName);
  inherited;
end;

function TMyObj.GetName: string;
begin
  Result:=FName;
end;

procedure TMyObj.SetName(const Name: string);
begin
  FName:=Name;
end;

【讨论】:

FWIW,桌面窗口不应用作对话框所有者 哎呀,在调试的这些年里,我养成了使用MessageBox(GetDesktopWindow,,,MB_OK or MB_SYSTEMMODAL) 的习惯,以确保它在所有情况下都能显示出来。我忘记了系统模式,但显然仍然使用 GetDesktopWindow。如果我没记错的话,也可以通过 0。 这是个坏主意。 Raymond Chen 对此进行了报道。

以上是关于避免嵌套的 try...finally 块在 Delphi的主要内容,如果未能解决你的问题,请参考以下文章

finally与return

对try catch finally的理解

try,finally问题

try finally return问题

try-catch- finally块中, finally块唯一不执行的情况是什么?

python的try finally (还真不简单)