Ответ 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;