How would you "extract" nested try/finally blocks from a routine into a reusable entity? Say I have
procedure DoSomething;
var
Resource1: TSomeKindOfHandleOrReference1;
Resource2: TSomeKindOfHandleOrReference2;
Resource3: TSomeKindOfHandleOrReference3;
begin
AcquireResource1;
try
AcquireResource2;
try
AcquireResource3;
try
// Use the resources
finally
ReleaseResource3;
end;
finally
ReleaseResource2;
end;
finally
ReleaseResource1;
end;
end;
and want something like
TDoSomething = record // or class
strict private
Resource1: TSomeKindOfHandleOrReference1;
Resource2: TSomeKindOfHandleOrReference2;
Resource3: TSomeKindOfHandleOrReference3;
public
procedure Init; // or constructor
procedure Done; // or destructor
procedure UseResources;
end;
procedure DoSomething;
var
Context: TDoSomething;开发者_如何学运维
begin
Context.Init;
try
Context.UseResources;
finally
Context.Done;
end;
end;
I want this to have the same exception-safety as the nested original. Is it enough to zero-initialize the ResourceN variables in TDoSomething.Init
and do some if Assigned(ResourceN) then
checks in TDoSomething.Done
?
There are three things about classes that make this idiom safe and easy:
- During the memory-allocation phase of the constructor (before the real constructor body runs), class-reference fields get initialized to nil.
- When an exception occurs in a constructor, the destructor is called automatically.
- It's always safe to call
Free
on a null reference, so you never need to checkAssigned
first.
Since the destructor can rely on all fields to have known values, it can safely call Free
on everything, regardless of how far the constructor got before crashing. Each field will either hold a valid object reference or it will be nil, and either way, it's safe to free it.
constructor TDoSomething.Create;
begin
Resource1 := AcquireResource1;
Resource2 := AcquireResource2;
Resource3 := AcquireResource3;
end;
destructor TDoSomething.Destroy;
begin
Resource1.Free;
Resource2.Free;
Resource3.Free;
end;
Use it the same way you use any other class:
Context := TDoSomething.Create;
try
Context.UseResources;
finally
Context.Free;
end;
Yes, you can use a single try/finally/end block for multiple resources with zero-initialization.
Another possible solution can be found in Barry Kelly blog
The pattern with testing on Assigned in finally is used in the Delphi source. You do kind of the same thing but I think you should move Context.Init to capture exception from Context.Init.
procedure DoSomething;
var
Context: TDoSomething;
begin
try
Context.Init;
Context.UseResources;
finally
Context.Done;
end;
end;
Edit 1 This is how you should do it without Context.Init and Context.Done. If you place all AquireResource code before try
you will not free Resource1 if you get an exception in AcquireResource2
procedure DoSomething;
var
Resource1: TSomeKindOfHandleOrReference1;
Resource2: TSomeKindOfHandleOrReference2;
Resource3: TSomeKindOfHandleOrReference3;
begin
Resource1 := nil;
Resource2 := nil;
Resource3 := nil;
try
AcquireResource1;
AcquireResource2;
AcquireResource3;
//Use the resources
finally
if assigned(Resource1) then ReleaseResource1;
if assigned(Resource2) then ReleaseResource2;
if assigned(Resource3) then ReleaseResource3;
end;
end;
精彩评论