开发者

Extract nested try/finally blocks

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:

  1. During the memory-allocation phase of the constructor (before the real constructor body runs), class-reference fields get initialized to nil.
  2. When an exception occurs in a constructor, the destructor is called automatically.
  3. It's always safe to call Free on a null reference, so you never need to check Assigned 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;
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜