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

时间:2013-08-28 19:55:20

标签: delphi interface try-catch

今天早上我有这个想法,避免嵌套尝试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;

我的问题是:这有用还是我忽略了什么?

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

9 个答案:

答案 0 :(得分:17)

是的,它有效。

在原始代码的嵌套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;

该库也不解决析构函数中的异常。

答案 1 :(得分:12)

我通常做这样的事情,因为它提供了代码可读性和复杂性之间的平衡:

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的{​​{1}}相同。

  • 但是,正如 Rob Kennedy 在他的评论中指出并在his answer中更详细地解释,它等同于使用T1的替代代码。< / p>

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

答案 2 :(得分:8)

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

ADUG 网站有一个Delphi implementation,源自 Barry Kelly's 有关如何实现强类型智能指针的文章Delphi使用泛型,匿名方法和接口:

  1. Smart pointers in Delphi
  2. Reference-counted pointers, revisited
  3. Somewhat more efficient smart pointers
  4. 您的代码将被重写为:

    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;
    

答案 3 :(得分:7)

我有一套辅助函数可以使@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)

答案 4 :(得分: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;

答案 5 :(得分:4)

是的,这段代码可行,但我个人倾向于将inherited添加到构造函数和析构函数中。

有很多库都有使用这种机制的实现。最新的移动平台Delphi编译器使用ARC自动引用计数来管理对象的生命周期,这种技术与编译器对对象引用的处理方法相同。

答案 6 :(得分:3)

这是同一个想法的略有不同的实现:

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来更容易控制。

答案 7 :(得分:1)

我认为不需要在界面中包含析构函数。默认情况下,Delphi在每个使用接口的过程/函数中构建一个幕后的try / finally,其中接口的引用计数减少,从而在它到达零时调用析构函数。

我有一个快速检查,但(至少在Delphi 7中)一个析构函数中的异常会阻止其他析构函数,遗憾的是。阻止这种情况的一种方法是在每个析构函数中编写try / except's,但这又是在其他地方更多的代码,只是为了首先保存代码......

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;

答案 8 :(得分:1)

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

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;

同样,如果没有更真实的场景,你很难理解它是如何工作的。我还在考虑一个很好的例子,当我想到一个时,我很乐意编辑。一般的想法是将业务规则的不同部分分成不同的可重用代码块。

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