Почему исключение не попало в попытку... кроме end ;?

У меня есть этот код (который работает под 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

Это ошибка (на самом деле, функция) на платформах iOS и Android (возможно, на других с LLVM-сервером, хотя они явно не документированы).

Основная проблема заключается в том, что исключение, вызванное вызовом виртуального метода на основе ссылки nil представляет собой аппаратное исключение, которое не захватывается ближайшим обработчиком исключений, и оно распространяется на следующий обработчик исключений (в данном случае обработчик исключения приложений).

Использовать вызов функции в блоке try-except, исключающем отказ от сбоев оборудования

С компиляторами для устройств 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 класса TFoo и 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;