为什么例外没有被尝试抓住...除了结束;?

lok*_*oki 15 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;
Run Code Online (Sandbox Code Playgroud)

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

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

我错过了什么?

Dal*_*kar 16

这是iOS和Android平台上的一个错误(实际上是一个功能)(可能是其他有LLVM后端的错误 - 尽管它们没有明确记录).

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

在try-except块中使用函数调用来防止未捕获的硬件异常

对于iOS设备的编译器,只有try块包含方法或函数调用时,块才能捕获硬件异常.这是与编译器的LLVM后端相关的差异,如果在try块中没有调用方法/函数,则无法返回.

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

var
  aData: IInterface;
begin
  try
    aData._Release;
  except
  end;
end;
Run Code Online (Sandbox Code Playgroud)

在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;
Run Code Online (Sandbox Code Playgroud)

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

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

var
  aData: TFoo;
begin
  try
    aData.Foo;
  except
  end;
end;
Run Code Online (Sandbox Code Playgroud)

即使我们更改Foo方法实现并从中删除所有代码,它仍将导致转义异常.


如果我们将Foo声明从虚拟更改为静态,则将正确捕获由除以零引起的异常,因为nil允许对引用上的静态方法进行调用,并且调用本身不会抛出任何异常 - 因此构成文档中提到的函数调用.

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

  TFoo = class(TObject)
  public
    procedure Foo; 
  end;
Run Code Online (Sandbox Code Playgroud)

另一个静态方法变体也导致正确处理的异常声明xTFoo类字段并在Foo方法中访问该字段.

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

procedure TFoo.Foo;
var
  x: integer;
begin
  x := 5;
end;
Run Code Online (Sandbox Code Playgroud)

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

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

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


解决方法:

代码中的一个变通方法可能nilnil在调用虚拟方法之前检查它.如果需要,在nil引用的情况下,我们也可以引发常规异常,通过封闭异常处理程序来正确捕获它.

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

文档中提到的另一种解决方法是将代码放入附加功能(方法)

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

var
  aData: NSData;
begin
  try
    SafeCall(aData);
  except
  end;
end;
Run Code Online (Sandbox Code Playgroud)