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

时间:2018-10-04 16:56:26

标签: delphi firemonkey delphi-10.2-tokyo

我有以下代码(在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

我错过了什么吗?

1 个答案:

答案 0 :(得分:16)

这是在iOS和Android平台上(可能是在其他具有LLVM后端的-尽管未明确记录)的 bug (实际上是功能)。

核心问题是,由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声明从虚拟更改为静态,则将正确捕获除数为零的异常,因为允许在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;