今天早上我有这个想法,避免嵌套尝试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;
我的问题是:这有用还是我忽略了什么?
对我而言,这看起来很酷,并且通过减少嵌套量使代码更容易阅读。它还可以扩展为存储一系列匿名方法,以便运行以执行诸如关闭文件,查询等操作...
答案 0 :(得分:17)
是的,它有效。
在原始代码的嵌套try-finally块和使用引用计数对象管理其他对象的生命周期的技术之间唯一不同的是,如果存在破坏任何对象的问题,会发生什么。如果在销毁任何对象时出现异常,嵌套的try-finally块将确保仍然释放任何剩余的对象。 TObjectList
中的TDoFinally
不会这样做;如果列表中的任何项目无法销毁,列表中的任何后续项目都将被泄露。
在实践中,这不是一个真正的问题。没有析构函数应该抛出异常。如果确实如此,那么无论如何都没有任何方法可以从中恢复,因此如果有任何泄漏则无关紧要。你的程序应该暂时终止,所以整理清理程序并不重要。
顺便提一下,JCL已经提供了ISafeGuard
和IMultiSafeGuard
接口来管理本地对象的生命周期。例如,您可以像这样重写代码:
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
抛出异常,T2
和T1
将不会被释放并导致内存泄漏,并且T2.Free
的{{1}}相同。
但是,正如 Rob Kennedy 在他的评论中指出并在his answer中更详细地解释,它等同于使用T1
的替代代码。< / p>
所以你的两种方法并不完全等同。
答案 2 :(得分:8)
Smart pointers是实现自动内存管理的另一种方式。
ADUG 网站有一个Delphi implementation,源自 Barry Kelly's 有关如何实现强类型智能指针的文章Delphi使用泛型,匿名方法和接口:
您的代码将被重写为:
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;
同样,如果没有更真实的场景,你很难理解它是如何工作的。我还在考虑一个很好的例子,当我想到一个时,我很乐意编辑。一般的想法是将业务规则的不同部分分成不同的可重用代码块。
或者,您可以将参数从一个过程传递到下一个过程,而不是声明全局变量。