【问题标题】:Delphi raise exception in constructorDelphi在构造函数中引发异常
【发布时间】:2016-08-23 20:19:46
【问题描述】:

情况

我要写一个类,构造函数是我自定义的,因为我需要初始化一些值。这是我目前写的代码:

type
 TCombinatorio = class(TObject)
  private
   valN, valK: integer;
   result: double;
  public
   property K: integer read valK;
   property N: integer read valN;
   constructor Create(valN: integer; valK: integer);
 end;

constructor TCombinatorio.Create(valN: Integer; valK: Integer);
begin
  inherited Create;
   Self.valN := valN;
   Self.valK := valK;

  if ((valN < 0) or (valK < 0)) then
   begin
    raise Exception.Create('N and K must be >= 0');
   end;

end;

由于我要进行一些数学计算,所以我需要避免使用负数。


问题

我可以这样在构造函数中引发异常吗?我是这样运行代码的:

procedure TForm1.Button1Click(Sender: TObject);
var a: TCombinatorio; 
    b: string;   
begin

 a := TCombinatorio.Create(5,-2);

 try
  //some code
 finally
  a.Free; 
 end;

end;

正如你在这里看到的,我的构造函数参数错误,因为第二个是负数。我也无法理解(根据我的构造函数的代码)是否真的需要 finally 中的a.Free,因为当构造函数引发异常时,会调用析构函数。

我想在 try-finally 块中包含 a := TCombinatorio.Create(5,-2); 以避免问题,但我不确定。你怎么看?

【问题讨论】:

    标签: delphi


    【解决方案1】:

    您的代码绝对正确。从构造函数中引发异常是完全值得尊重的。如您所知,析构函数被调用。

    你问这个代码:

    a := TCombinatorio.Create(5,-2);
    try
      //some code
    finally
      a.Free; 
    end;
    

    你担心Free 会在对象已经被销毁后被调用。那不可能发生。如果在构造函数中引发异常,则它会向上传播调用堆栈。这发生在try 块开始之前,因此finally 块不会执行。事实上,分配给a 并没有发生。

    在try 中移动创建将是灾难性的,实际上是一个非常常见的错误。假设你这样做了:

    // WARNING THIS CODE IS DEFECTIVE 
    try
      a := TCombinatorio.Create(5,-2);
      //some code
    finally
      a.Free; 
    end;
    

    现在,如果引发异常,则调用 Free,但在什么情况下?变量a 未初始化。即使它是,它不是,那仍然是双重免费的。

    【讨论】:

    • @kludg - 如果您在构造时知道实例将无法使用,构造函数是引发异常的最佳位置 imo。另一种方法是在实例被使用时引发异常,这可能会导致有趣的调试会话来了解错误的来源。也就是说,在这种特定情况下,OP 应该切换到无符号整数。
    • @kludg:我完全不同意。请注意,引用类型的类字段(如TObject)始终使用nil 进行初始化,这与局部变量不同。在你的析构函数中调用 myAggregatedObject.Free() 就可以了。但是,您应该避免在构造函数中调用虚方法,但这是完全不同的故事,不仅适用于 Delphi。
    • @serg 在析构函数中引发异常是错误的。但它是为构造函数明确设计和支持的。你不写你的析构函数来处理部分构造吗?
    • @LievenKeersmaekers。类可以处理负数。它说他们是不允许的。如果您在尝试使用普通整数(不可避免地会这样)创建它的构造函数时只处理无符号的,则需要检查该数字是否为负数。默认情况下,编译器只能提供警告帮助,您无法在编译时知道整数将取什么值。它可能总是有效的。它可能不会。建议的方法的唯一替代方法是将测试放在创建对象的每个代码实例之外,现在和永远。
    • @Dsm - 如果您启用了范围检查错误(我相信您应该这样做),您将在尝试传递负值时遇到错误。自己测试和引发错误只是开销。如果一个人在编译他的程序时没有出现范围检查错误,那么所有的赌注都没有了,但在这种情况下,很可能程序中还有许多其他细微的错误等待被发现。
    【解决方案2】:

    好的,首先您可以在构造函数中引发异常,是的,它确实会调用析构函数。您显示的代码很好。但我认为你误解了你的代码的作用。并且将构造函数放在 try finally 块中是错误的。我认为您缺少的一点是,如果您的构造函数失败,则 try...finally 块永远不会被执行,因此 free 不会被执行。如果构造函数不成功,你不应该调用 free,这就是为什么你应该不将构造函数放在 try...finally 块中。

    【讨论】:

      【解决方案3】:

      首先我要说的是,你不能避免构造函数中的异常,所以它不能是一个反模式。如果您检查 Delphi 源代码,您会发现在构造函数中引发异常的地方很多。例如

      constructor TCustomForm.Create(AOwner: TComponent);
      begin
        // ... skipped some lines
              if not InitInheritedComponent(Self, TForm) then
                raise EResNotFound.CreateFmt(SResNotFound, [ClassName]);
      

      您唯一应该知道的是,如果异常从构造函数中逃脱,Delphi 将自动调用 析构函数。实际上,这意味着您的 destructor 可能会在部分构造的对象上执行,而正确编写 destructor 是您的责任。请参阅TObject.Destroy 文档,并特别注意以下引用:

      注意:如果异常从构造函数中逃逸,则调用析构函数销毁部分构造的对象实例 未能完全初始化。因此,析构函数应该检查 实际分配了句柄等分配的资源 在尝试释放它们之前,因为它们的值可能为零。

      PS 一般来说,您应该假设每一行代码都可能引发异常,但请不要妄想;)

      【讨论】:

      • 我要补充一点,如果您这样做(在 ctor 中引发异常),您将不得不在客户端应用程序中的每次调用之前测试您的对象是否已创建!因为如果发生异常 --> 自动调用析构函数 --> object = nil --> object.DoSomething 会导致访问冲突
      • 通常不需要测试它,因为你的object.DoSomething代码不应该因为异常而被执行。如果您可以在需要此检查的地方发布代码示例,我将不胜感激。我会假设您已经手动处理了构造函数异常,或者您的代码可以从析构函数中调用。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-09-20
      • 1970-01-01
      • 2016-10-02
      • 2018-11-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多