Студенты, ничего не хочу сказать - часто встречаются очень светлые головы, но я давно заметил, что ошибки, которые они совершают, не всегда просто выявить.
Ситуации, с которыми мне приходилось сталкиваться были иногда настолько экзотическими, что заставляли реально напрягаться с тем, чтобы найти-таки причину проблемы.
Причина этой экзотики часто состоит в том, что они (студенты) в силу своей неопытности легко делают вещи, которые вы делать уж точно не станете, причём совершенно автоматически.
Вот например, сегодняшний случай...
Фрагмент кода, приводимый ниже приводит к возбуждению Access Violation при выходе их процедуры при условии, что используется FastMM4 и режим FullDebugMode (студенты выполняют разработку именно в этом режиме, для того, чтобы сразу вычищать грязь в коде, по крайней мере такую, которую способен выявить менеджер кучи):
procedure Execute;
var
dbSchemaSpec: TDBSchemaSpec;
tableSpec: TTableSpec;
intfDBSchemaSpec: IDBSchemaSpec;
intfTableSpec: ITableSpec;
begin
dbSchemaSpec := createSchema;
tableSpec := TTableSpec.Create;
tableSpec.SetName('N3');
try
dbSchemaSpec.AddTable(tableSpec);
dbSchemaSpec.AddTable(createTable('N1'));
dbSchemaSpec.AddTable(createTable('N2'));
dbSchemaSpec.AddDomainSpec(createDomain);
intfDBSchemaSpec := dbSchemaSpec;
writeln(intfDBSchemaSpec.GetDomainSpecCount);
writeln(intfDBSchemaSpec.GetDomainSpec(0).GetDomainName); // <-- problem here!
writeln(intfDBSchemaSpec.GetTableSpecCount);
writeln(intfDBSchemaSpec.GetTableSpec(0).GetName);
writeln(intfDBSchemaSpec.GetTableSpec(1).GetName);
if intfDBSchemaSpec.TryGetTableSpecByName('N3', intfTableSpec) then
writeln(intfTableSpec.GetName);
if intfDBSchemaSpec.TryGetTableSpecByName('N1', intfTableSpec) then
writeln(intfTableSpec.GetName, ' ', intfTableSpec.GetIndexSpecCount);
finally
intfDBSchemaSpec := nil;
intfTableSpec := nil;
dbSchemaSpec.Free;
end;
writeln('close finally');
end;
Суть выполняемых действий - создать некоторое множество связанных объектов (классы TDBSchemaSpec, TTableSpec) затем - поработать с ними с помощью реализованных в них интерфейсов.
Упомянутые классы содержат собственную реализацию IInterface, обеспечивающую их "выживание" при финализации интерфейсных ссылок на них
ПоказатьСкрыть
unit InterfacedBaseClass;
interface
type
TInterfacedBaseClass = class(TObject, IInterface)
public
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
end;
implementation
function TInterfacedBaseClass.QueryInterface;
begin
if GetInterface(IID, Obj) then
Result := 0
else
Result := E_NOINTERFACE;
end;
function TInterfacedBaseClass._AddRef: Integer;
begin
Result := -1;
end;
function TInterfacedBaseClass._Release: Integer;
begin
Result := -1;
end;
end.
Для того, чтобы иметь свободу эту реализацию изменить, или предоставить альтернативную, обеспечивающую возможность работы в том же программном интерфейсе, но в иных условиях (например, в ситуации, когда непосредственного доступа к интересующим данным нет, и их приходится запрашивать с сервера приложений).
Но вернёмся к AV. Под отладчиком было отчётливо видно, что исключение возбуждается в System._IntfClear, в строке:
CALL DWORD PTR [EAX] + VMTOFFSET IInterface._Releaseчто прямо указывает на попытку вызова метода _Release у уже освобождённого объекта.
Вне всякого сомнения, Вы Уважаемый читатель, уже догадались, в чём суть происходящего :-)
Но для людей, которые соображают не столь быстро (для таких как я, например) я продолжу.
Так вот, совершенно понятно, что такое происходит практически всегда, когда объект освобождён, а существующая на него интерфейсная ссылка ещё не вышла из контекста своего определения (или из "области видимости", как принято часто говорить, хотя в данном случае это чушь :-)). Но что это за ссылка?! WTF?!
В процедуре Execute определены две ссылки, которые могли бы претендовать на эту роль:
intfDBSchemaSpec: IDBSchemaSpec; intfTableSpec: ITableSpec;но обе они старательно инициализированы молодым человеком в секции finally, именно в свете обозначенных выше их особенностей!
Я посмотрел на часы, у меня оставалось меньше получаса.
Опущу подробности, в конце концов мне было очевидно, что проблема в какой-нибудь ерунде - я сосредоточился на том, чтобы объяснить людям, как локализовать ошибку. Т.е. дистиллировать ситуацию, довести набор действий, приводящих к проблеме до абсолютного минимума с тем, чтобы их исследовать для выявления причин происходящего. Все эти объяснения заняли некоторое время, а перерывом мне воспользоваться не дали, хотя я уже догадывался в чём дело, поскольку в процессе дистилляции стало очевидно, что ошибка вызвана кодом, отмеченным комментарием "// <-- problem here!". Отчётливое понимание пришло уже в фойе - пришлось развернуться, найти молодого человека и объяснить ему в чём дело и как с этим быть.
Собственно, объяснение очень простое.
Всё верно, в коде действительно не объявлено интерфейсных ссылок, которые нуждались бы в инициализации. Такие ссылки появились неявно, и мне известен только один способ их инициализировать :-) Приведу здесь "в тему" небольшой инсайд - выдержку из нашего корпоративного документа, описывающего некоторые особенности работы с интерфейсами в Delphi:
«Свойства типа интерфейс
Создание свойств типа интерфейс провоцирует к написанию кода следующего вида:
Таким образом, для возвращения интерфейса лучше использовать функции с мнемоническим названием вида GetIXXX, что бы вызывающий функцию программист "знал" (или догадался), что ему вернут интерфейс и он должен с ним обойтись подобающим образом.»Создание свойств типа интерфейс провоцирует к написанию кода следующего вида:
type
ISomeInterface = interface
...
procedure SomeProcedure;
...
end;
TSomeClass = class(TSomeParentClass)
...
protected
function GetISomeInterface: ISomeInterface;
public
property SomeInterface: ISomeInterface read GetISomeInterface;
end;
...
procedure...
var
instance: TSomeClass;
begin
instance := TSomeClass.Create;
try
// использование someClass
instance.SomeInterface.SomeProcedure; // (*)
finally
instance.Free;
end;
end;
(*) здесь, неявно (т.е. уже противоречит дзену Питона), была создана переменная типа интерфейс, увеличен её счетчик ссылок, а уменьшен он будет только при вы ходе из процедуры, т.е. после уничтожения самого объекта.
Обсуждаемый случай совершенно аналогичен ситуации, изложенной в корпоративном документе. Всё та же неявная интерфейсная ссылка, появляющаяся при обращении к свойству (или методу), возвращающему интерфейс, к которой нет доступа для того, чтобы её инициализировать. Всё-тот же объект, в контексте которого существует то, на что направлена эта ссылка, и всё то же освобождение этого объекта, влекущее за собой освобождение того, на что направлена ссылка, что делает ссылку "провисшей" с неминуемой попыткой вызова _Release уже освобождённого объекта при выходе из процедуры.
Собственно, с вопросом "почему?" надеюсь, всё относительно понятно.
Теперь несколько слов относительно вопроса "Как?". В смысле "Как с этим жить?".
Здесь всё тоже относительно просто. - Не делать так. Не смешивать в одной процедуре объектную технику с интерфейсной, если объектным и интерфейсным ссылкам соответствуют одни и те же экземпляры классов.
В сущности, основная масса проблем вызывается тем, что при таком "смешении" возникает ситуация, когда объект явным образом освобождается - т.е. для объектной ссылки вызывается метод Free (ну, или Destroy у тех, кто любит острые ощущения).
К сожалению, часто оказывается сложным почувствовать опасность в каждом конкретном случае. Ну, для меня не очевидно хотя бы то, что место, отмеченное мною "// <-- problem here!" приводит к появлению неявной интерфейсной ссылки - для этого нужно знать, что метод IDBSchemaSpec.GetDomainSpec(0) возвращает интерфейс, а потом вспомнить, к чему это приводит (к появлению неявной интерфейсной ссылки). Сам я стараюсь следовать следующей парадигиме. Если в коде процедуры используется объектная ссылка у которой запрашивается интерфейс, то запрос интерфейса и его использование должно производиться в отдельной процедуре.
Например, если оригинальный исходный текст изменить способом, приведённым ниже, проблем не возникнет:
procedure Execute;
procedure ExecuteInterfaceTests(ADBSchemaSpec: IDBSchemaSpec);
var
intfTableSpec: ITableSpec;
begin
writeln(ADBSchemaSpec.GetDomainSpecCount);
writeln(ADBSchemaSpec.GetDomainSpec(0).GetDomainName);
writeln(ADBSchemaSpec.GetTableSpecCount);
writeln(ADBSchemaSpec.GetTableSpec(0).GetName);
writeln(ADBSchemaSpec.GetTableSpec(1).GetName);
if ADBSchemaSpec.TryGetTableSpecByName('N3', intfTableSpec) then
writeln(intfTableSpec.GetName);
if ADBSchemaSpec.TryGetTableSpecByName('N1', intfTableSpec) then
writeln(intfTableSpec.GetName, ' ', intfTableSpec.GetIndexSpecCount);
end;
var
dbSchemaSpec: TDBSchemaSpec;
tableSpec: TTableSpec;
begin
dbSchemaSpec := createSchema;
tableSpec := TTableSpec.Create;
tableSpec.SetName('N3');
try
dbSchemaSpec.AddTable(tableSpec);
dbSchemaSpec.AddTable(createTable('N1'));
dbSchemaSpec.AddTable(createTable('N2'));
dbSchemaSpec.AddDomainSpec(createDomain);
ExecuteInterfaceTests(dbSchemaSpec);
finally
dbSchemaSpec.Free;
end;
writeln('close finally');
end;
Неявные интерфейсные ссылки как появлялись, так и появляются, но происходит это в контексте процедуры ExecuteInterfaceTests. И время жизни таких ссылок ограничено этой процедурой.Но поскольку выполнение ExecuteInterfaceTests завершается до освобождения объекта dbSchemaSpec, на детали которого направлены интерфейсные ссылки, всё проходит благополучно.
Кстати, в теле ExecuteInterfaceTests уже не нужен и блок try..finally, и инициализация объявленных в этой процедуре интерфейсных ссылок.
Небольшая ремарка в конце. Возражение на возражение тех, кому лень объявлять отдельную процедуру.
Это касается начинающих разработчиков.
ПоказатьСкрыть
Обычно, это объясняется так: "Ну уж я-то этого не забуду." Или: "Я не выполняю действий, приводящих к появлению неявных интерфейсных ссылок." Или: "Я не собираюсь здесь освобождать объект."
Знаете, по слухам, у эскимосов (те, что на Аляске живут, на Чукотке живут чукчи) есть такая присказка: "Тебя убьёт не тот медведь, которого ты видишь..." Таким образом подчёркивается ум, хитрость и коварство белого медведя. Один - отвлекает внимание, возможно - угрожает, а второй - заходит сбоку или сзади, после чего его атака оказывается неотразимой. Едят лузера они, вероятно сообща... - По-моему, нет ничего зазорного в том, чтобы на минутку почувствовать себя эскимосом. тем, кто повторяет эту присказку, не тем, кого съели ;-)
Дело в том, что забудет (точнее - не узнает), выполнит (потому, что не в контексте) и соберётся (т.е. освободит) - не Вы, а другой человек. Он точно это сделает - всё будет как у Чехова: Вы в первом акте вешаете на стену ружьё (смешиваете в одном коде объектную и интерфейсную техники), а в последнем акте (у пользователя, возможно на другом конце света) это ружьё "стреляет" - бедняга получает вместо запрошенного, возможно, критически важного для него действия - Access Violation.
И виноваты в этом будете Вы. Не потому, что допустили ошибку, но потому, что спровоцировали её, сделали возможной, точнее - вероятной. "Расставили грабли" словом...
Бывают ли исключения из обозначенного правила? - Конечно бывают. Вы сами к ним придёте, когда перестанете использовать объяснения, вроде приведённых выше :-)
Знаете, по слухам, у эскимосов (те, что на Аляске живут, на Чукотке живут чукчи) есть такая присказка: "Тебя убьёт не тот медведь, которого ты видишь..." Таким образом подчёркивается ум, хитрость и коварство белого медведя. Один - отвлекает внимание, возможно - угрожает, а второй - заходит сбоку или сзади, после чего его атака оказывается неотразимой. Едят лузера они, вероятно сообща... - По-моему, нет ничего зазорного в том, чтобы на минутку почувствовать себя эскимосом. тем, кто повторяет эту присказку, не тем, кого съели ;-)
Дело в том, что забудет (точнее - не узнает), выполнит (потому, что не в контексте) и соберётся (т.е. освободит) - не Вы, а другой человек. Он точно это сделает - всё будет как у Чехова: Вы в первом акте вешаете на стену ружьё (смешиваете в одном коде объектную и интерфейсную техники), а в последнем акте (у пользователя, возможно на другом конце света) это ружьё "стреляет" - бедняга получает вместо запрошенного, возможно, критически важного для него действия - Access Violation.
И виноваты в этом будете Вы. Не потому, что допустили ошибку, но потому, что спровоцировали её, сделали возможной, точнее - вероятной. "Расставили грабли" словом...
Бывают ли исключения из обозначенного правила? - Конечно бывают. Вы сами к ним придёте, когда перестанете использовать объяснения, вроде приведённых выше :-)
PS:
ПоказатьСкрыть
Ну какого чёрта не сделать автоматическую финализацию этих неявных интерфейсных ссылок?
На уровне компилятора, которому известен контекст?
Ну ведь совершенно понятно, что если запрос интерфейса производится в процессе цепочки вызовов, результатом этого запроса (интерфейсной ссылкой) нельзя воспользоваться за рамками этой цепочки!
Так что же мешает в описанном сценарии эту ссылку автоматически исключить из контекста, а не откладывать это до завершения процедуры?!
Кто бы сказал... :-((
На уровне компилятора, которому известен контекст?
Ну ведь совершенно понятно, что если запрос интерфейса производится в процессе цепочки вызовов, результатом этого запроса (интерфейсной ссылкой) нельзя воспользоваться за рамками этой цепочки!
Так что же мешает в описанном сценарии эту ссылку автоматически исключить из контекста, а не откладывать это до завершения процедуры?!
Кто бы сказал... :-((
Комментариев нет :
Отправить комментарий