为什么异常没有被 try...except end; 捕获?

Why the exception is not caught by the try... except end;?

我有这段代码(在 iOS 和 Delphi 东京运行):

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 处理程序。

Access violation at address 0000000100EE9A8C, accessing address 0000000000000000

我是不是漏掉了什么?

这是 bug(实际上是 feature)在 iOS 和 Android 平台(可能在具有 LLVM 后端的其他人上——尽管它们没有明确记录)。

核心问题是 nil 引用上的虚方法调用引起的异常构成了硬件异常,它没有被最近的异常处理程序捕获,并且传播到下一个异常处理程序(在本例中为应用程序异常处理程序).

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

With compilers for iOS devices, except blocks can catch a hardware exception only if the try block contains a method or function call. This is a difference related to the LLVM backend of the compiler, which cannot return if no method/function is called in the try block.

在 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 class 字段并在 Foo 方法中访问该字段。

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

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

回到涉及 NSData 引用的原始问题。 NSData 是 Objective-C class 并且这些在 Delphi.

中表示为接口
  // root interface declaration for all Objective-C classes and protocols
  IObjectiveC = interface(IInterface)
    [IID_IObjectiveC_Name]
  end;

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


解决方法:

其中引用可能是 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;