【问题标题】:Why the exception is not caught by the try... except end;?为什么 try... except end; 没有捕获到异常?
【发布时间】:2018-10-04 16:56:25
【问题描述】:

我有这段代码(在 iOS 和 Delphi Tokyo 下运行):

procedure TMainForm.Button1Click(Sender: TObject);
var aData: NSData;
begin    
  try    
      try
        aData := nil;
      finally
        // this line triggers an exception
        aData.release;
      end;    
  except
    on E: Exception do begin
      exit;
    end;
  end;

end;

通常应该在except end 块中捕获异常,但在这种情况下,它不会被处理程序捕获,而是传播到Application.OnException 处理程序。

地址0000000100EE9A8C的访问冲突,访问地址 0000000000000000

我错过了什么吗?

【问题讨论】:

  • 在 AV 发生之前执行的最后一行是什么?
  • @DavidHeffernan 这是一个恰当的例子。如果您尝试在 iOS 上运行该代码,则异常处理程序不会捕获异常。
  • @RudyVelthuis 但这真的可以解释Application.OnException 事件在地址 0 处捕获 AV 吗?这表明release() 正在触发一个异步 消息,该消息在Button1Click 退出后得到处理(并因此崩溃)
  • FWIW,@TheBitman:在 Delphi for Windows(和 VCL)中发布与在 NSObject 派生类(如 iOS 或 macOS 中的 NSData)上发布几乎不同。完全不同的东西。
  • @TheBitman:如果您希望所有人都能看到您删除的答案,请取消删除。但是您很可能会删除它并停止修改它。 NSData 是一个Objective-C 对象(与Delphi 对象几乎不一样),封装在一个实现NSData 接口的Delphi 类中(因此Delphi 可以使用它)。显然你对iOS一无所知,而TCustomForm.Release与NSObject.release完全无关。后者更像是接口的发布,用于手动引用计数。甚至调用方法的 NSObject 消息传递也与 Windows 中的消息不完全相同。

标签: delphi firemonkey delphi-10.2-tokyo


【解决方案1】:

这是 iOS 和 Android 平台上的一个错误(实际上是一个功能)(可能在其他具有 LLVM 后端的平台上——尽管没有明确记录)。 p>

核心问题是nil引用上的虚方法调用导致的异常构成硬件异常,该异常未被最近的异常处理程序捕获,并传播到下一个异常处理程序(在本例中为应用程序异常处理程序)。

Use a Function Call in a try-except Block to Prevent Uncaught Hardware Exceptions

使用 iOS 设备的编译器,除了块可以捕获硬件 仅当 try 块包含方法或函数调用时才会出现异常。 这是与编译器的 LLVM 后端相关的差异, 如果在 try 块中没有调用任何方法/函数,则无法返回。

在 iOS 和 Android 平台上显示该问题的最简单代码是:

var
  aData: IInterface;
begin
  try
    aData._Release;
  except
  end;
end;

在 Windows 平台上执行上述代码按预期工作,异常被异常处理程序捕获。上面的代码中没有nil 赋值,因为aData 是接口引用,它们在所有平台上都会被编译器自动取消。添加nil 分配是多余的,不会改变结果。


为了表明异常是由虚方法调用引起的

type
  IFoo = interface
    procedure Foo;
  end;

  TFoo = class(TInterfacedObject, IFoo)
  public
    procedure Foo; virtual;
  end;

procedure TFoo.Foo;
var
  x, y: integer;
begin
  y := 0;
  // division by zero causes exception here
  x := 5 div y;
end;

在以下所有代码变体中,异常会转义异常处理程序。

var
  aData: IFoo;
begin
  try
    aData.Foo;
  except
  end;
end;

var
  aData: TFoo;
begin
  try
    aData.Foo;
  except
  end;
end;

即使我们改变Foo的方法实现并从中删除所有代码,仍然会导致转义异常。


如果我们将 Foo 声明从 virtual 更改为 static,除零引起的异常将被正确捕获,因为在 nil 引用上调用静态方法是允许的,并且调用本身不会抛出任何异常 - 因此构成函数调用文档中提到。

type
  TFoo = class(TInterfacedObject, IFoo)
  public
    procedure Foo; 
  end;

  TFoo = class(TObject)
  public
    procedure Foo; 
  end;

另一个导致异常的静态方法变体也可以正确处理,是将x 声明为TFoo 类字段并在Foo 方法中访问该字段。

  TFoo = class(TObject)
  public
    x: Integer;
    procedure Foo; 
  end;

procedure TFoo.Foo;
var
  x: integer;
begin
  x := 5;
end;

回到涉及NSData 参考的原始问题。 NSData 是 Objective-C 类,在 Delphi 中表示为接口。

  // root interface declaration for all Objective-C classes and protocols
  IObjectiveC = interface(IInterface)
    [IID_IObjectiveC_Name]
  end;

由于在接口引用上调用方法始终是通过 VMT 表的虚拟调用,因此在这种情况下,其行为方式与直接在对象引用上调用的虚拟方法调用类似(表现出相同的问题)。调用本身会引发异常,并且不会被最近的异常处理程序捕获。


解决方法:

引用可能是nil 的代码中的一种解决方法是在对其调用虚拟方法之前检查nil。如果需要,在 nil 引用的情况下,我们还可以引发常规异常,这些异常将被封闭的异常处理程序正确捕获。

var
  aData: NSData;
begin
  try
    if Assigned(aData) then
      aData.release
    else
      raise Exception.Create('NSData is nil');
  except
  end;
end;

文档中提到的另一种解决方法是将代码放在附加函数(方法)中

procedure SafeCall(const aData: NSData);
begin
  aData.release;
end;

var
  aData: NSData;
begin
  try
    SafeCall(aData);
  except
  end;
end;

【讨论】:

  • 非常好的文章@dalijaPrasnikar!我很想知道为什么 emb 决定这样做,因为这是他们故意这样做的接缝..
  • @lok​​i 如果您阅读了我从文档中引用的行 - 这是因为如果 try 块内没有函数,LLVM 后端无法返回到 except 块。这意味着 Delphi 编译器将不得不伪造函数调用,这将对编译时间和运行时带来性能损失。 (多少,我不知道)但我也非常不喜欢这种“错误/功能”行为。
  • :我认为当我们写 try .. 除了 end 我们已经意识到它们可能会受到一些性能损失,而现在使用更新的 CPU 我认为这种“跳跃”损失将非常小,我们甚至不会注意到它
  • 还有一个问题是在 LLVM 后端实现这种异常处理有多容易。解决 LLVM 方面的问题可能不会发生(至少不会很快发生)这个关于问题的错误/功能报告自 2007 年以来就在他们的错误跟踪器中Add support for asynchronous "non-call" exceptions, eliminate invoke/call dichotomy
猜你喜欢
  • 2011-11-28
  • 1970-01-01
  • 1970-01-01
  • 2010-09-07
  • 1970-01-01
  • 2021-06-27
  • 2019-07-12
  • 2022-01-06
  • 2023-02-06
相关资源
最近更新 更多