【问题标题】:Extract nested try/finally blocks提取嵌套的 try/finally 块
【发布时间】:2011-04-07 13:31:18
【问题描述】:

如何将嵌套的 try/finally 块从例程“提取”到可重用实体中?说我有

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;

想要类似的东西

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;

我希望它具有与嵌套原始文件相同的异常安全性。将TDoSomething.Init 中的ResourceN 变量初始化为零并在TDoSomething.Done 中进行一些if Assigned(ResourceN) then 检查就足够了吗?

【问题讨论】:

  • @Mr.失望:如果它减轻了你的痛苦,想象一下三个嵌套块被提取到三个例程中,然后以嵌套方式调用它们。 :-) 不会改变问题的核心。
  • 嘿 - 评论去哪儿了? :-)
  • 确实如此 - 当我克服“天哪!”时,我删除了评论由于我不是Delphi的家伙,那一刻还算快,突然回忆起来差了很多。 ;) 但是,我会问:你为什么不能简单地使用一个try / finally,确定哪些资源没有被获取,然后处理那些资源?不过,我想这就是您要采用的方法。
  • 是的。我只是问,因为我不想无意中将异常安全抛到窗外。

标签: delphi refactoring extract try-finally


【解决方案1】:

关于类的三点让这个习语变得安全和简单:

  1. 在构造函数的内存分配阶段(在真正的构造函数主体运行之前),类引用字段被初始化为 nil。
  2. 当构造函数发生异常时,会自动调用析构函数。
  3. 在空引用上调用Free 总是安全的,因此您无需先检查Assigned

由于析构函数可以依赖所有字段来获得已知值,因此它可以安全地在所有内容上调用Free,无论构造函数在崩溃前走了多远。每个字段要么持有一个有效的对象引用,要么为零,无论哪种方式,释放它都是安全的。

constructor TDoSomething.Create;
begin
  Resource1 := AcquireResource1;
  Resource2 := AcquireResource2;
  Resource3 := AcquireResource3;
end;

destructor TDoSomething.Destroy;
begin
  Resource1.Free;
  Resource2.Free;
  Resource3.Free;
end;

像使用任何其他类一样使用它:

Context := TDoSomething.Create;
try
  Context.UseResources;
finally
  Context.Free;
end;

【讨论】:

  • 我不能利用第 3 项,因为 ReleaseResource 不一定是对 TObject.Free 的调用。但我可以用if Assigned 解决这个问题,无论如何,第 2 项似乎是重点。所以我会坚持惯用的方式并从中受益。
  • 我通常将构造函数调用放在 try..finally 块中。我刚刚意识到(根据您的第 2 点)没有必要。不过,我不确定我是否会改变我的习惯,因为我发现它更清楚了。你怎么看?
  • 按照 TObject.FreeFreeMemmake ReleaseResource 的示例安全调用空资源。它使其他地方的事情变得如此简单。
  • 不仅没有必要,@PA,而且是错误的。如果构造函数抛出异常,则您分配结果的变量未初始化,因此您在未初始化的值上调用 Free
【解决方案2】:

是的,您可以将单个 try/finally/end 块用于零初始化的多个资源。

另一种可能的解决方案可以在Barry Kelly blog中找到

【讨论】:

  • 感谢您的想法。某种守卫界面已经出现在我的脑海中,但对我来说似乎总是矫枉过正。顺便说一句:我评论了巴里的帖子。 :-)
【解决方案3】:

Delphi 源代码中使用了最后对 Assigned 进行测试的模式。你做了同样的事情,但我认为你应该移动 Context.Init 以从 Context.Init 捕获异常。

procedure DoSomething;
var
  Context: TDoSomething;
begin
  try
    Context.Init;
    Context.UseResources;
  finally
    Context.Done;
  end;
end;

编辑 1 在没有 Context.Init 和 Context.Done 的情况下,您应该这样做。如果您将所有 AquireResource 代码放在 try 之前,如果您在 AcquireResource2 中遇到异常,您将不会释放 Resource1

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;

【讨论】:

  • 嗯,将 Init 放在 try 块中似乎是错误的。您是否建议这样做是因为我将 TDoSomething 设为记录类型。如果是一堂课,你会写try Context := TDoSomething.Create;吗?
  • 它似乎错了,因为它错了。如果初始化抛出异常,你不想完成任何事情,因为你不知道什么是安全的。
  • @Mikael:这可能与stackoverflow.com/q/398137/35162 中的问题相同。我记得有很多关于非常微妙点的讨论。 ;-)
  • @Ulrich - 是的,接受的答案与我建议的相同。这是否意味着您的问题应该被标记为重复:)?
  • 不,Mikael,接受的答案与您在此处显示的答案相同。如果AcquireResource2抛出异常,Resource2Resource3还不会被初始化,所以用Assigned检查它们的当前值是错误的。您需要在进入try 块之前进行初始化。
猜你喜欢
  • 1970-01-01
  • 2013-09-01
  • 1970-01-01
  • 2018-11-07
  • 2012-06-17
  • 1970-01-01
  • 2020-08-24
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多