Motto

В тихом саду здравомыслия
Пусть на вас постоянно падают
кокосовые орехи пробужденности.
Чогьям Трунгпа РИНПОЧЕ


Версия для мобильного


пятница, 24 апреля 2009 г.

Текучка 10: Что может случиться если сначала обнулять переменную, а потом вызывать деструктор.

Прочитал на днях пост GunSmoker-a о том, что всегда нужно использовать FreeAndNil вместо Free. Прочитал, подивился, взял на заметку попробовать завести такую привычку, хотя так и не смог придумать объяснения, кроме как: а почему бы и нет?

И вот бывает же! Как раз сегодня наткнулся на ситуацию, когда из деструктора объекта происходило обращение к самому объекту по имени переменной. Конечно обращение происходило не из самого объекта, а из одного из вложенных. Кстати, дело было в коде Lazy Delphi Builder.=) Из-за частых изменений в иерархии классов, и постоянной работе над проектом в режиме “скорей бы уже релиз выпустить” у меня там развёлся довольно милый бардачок, с большим количеством глобальных переменных.

Итак..

Итак, есть у меня глобальный объект LazyInstaller, который в конструкторе создаёт другой глобальный объект – главную форму. Форма довольно активно использует LazyInstaller, а LazyInstaller изредка обращается к форме. Так вот следуя правилу “кто объект создаёт, тот его и разрушает”, у LazyInstaller-а в деструкторе прописано уничтожение главной формы. Форма в свою очередь уничтожает VirtualTreeView, а VirtualTreeView, при уничтожении вызывал событие OnFreeNode, обработчик которого, в свою очередь обращался к LazyInstaller-у, который в тёмном чулане хранится в доме, который построил Джек.

До тех пор пока LazyInstaller освобождался так

LazyInstaller.Free;
LazyInstaller:=nil;

всё работало нормально. Вызываемый метод просто возвращал nil, который проверялся в обработчике события и всё вроде работало нормально.

В рамках очередного рефакторинга, я заменил тип переменной с объектной на интерфейсную. Соответственно изменилось и уничтожение. Если раньше было:

var
  LazyInstaller : TLazyInstaller;
...
LazyInstaller .Free;
LazyInstaller :=nil;

А после замены на интерфейс стало:

var
  LazyInstaller : ILazyInstaller;
...
LazyInstaller :=nil;

Случилось то же самое, что и при использовании FreeAndNil. Сначала переменная LazyInstaller становилась равной nil, и уже после этого вызывался деструктор объекта.

Так что, при выходе из программы стал появляться загадочный AV. Замучался его выслеживать. Только после того, как понаставил повсюду assert(assigned(LazyInstaller)) смог определить это место. Ещё одна такая ошибка и начну заменять глобальные переменные на функции, чтобы проверять assert(assigned(LazyInstaller)) в одном месте, а не рыскать по всему коду. =)

6 комментариев:

  1. А вот чтобы не мучаться и не искать источник AV, надо использовать EurekaLog ;) (ладно, JCL и madExcept подойдут тоже :D )

    P.S.
    Ссылка битая: l забыл в html.

    ОтветитьУдалить
  2. Хммм... Не думаю, что EurekaLog, JCL или madExcept в этом случае могут показать бы больше чем Call Stack в IDE. На EurekaLog в моём бюджете не запланировано выделение средств.=)) А JCL для моего open-source-ного проекта очень даже подойдут.
    p.s. Спасибо, исправил.

    ОтветитьУдалить
  3. Ну, это была почти шутка ;)
    На самом деле, достаточно же поймать это AV в отладчике, после чего сделать останов и посмотреть, в какую строчку нам ткнули. Ну и стек ещё можно посмотреть (View/Debug windows/Call stack) - и всё, проблема найдена.
    Другой вопрос, конечно же, - понять _причину_ (а вернее - источник) проблемы.
    Но, вот в данном примере, если мы увидели AV при обращение по объектной ссылке ("причина") - то нам всего-лишь нужно поставить логгинг на создание/удаление этого объекта. Тогда мы сразу увидим, что объект удаляется раньше, чем мы доходим до проблемного места ("источник").

    Кстати, вот буквально вчера-сегодня решали полностью аналогичную проблему: http://delphikingdom.ru/asp/answer.asp?IDAnswer=70150
    Только в этот раз для поиска источника использовался FastMM в FullDebugMode (потому что AV не было, в отличие от твоего случая).

    ОтветитьУдалить
  4. Спасибо, мне понравилось как вы её решали. Узнал много нового про FastMM.

    ОтветитьУдалить
  5. На самом деле вызов объекта-прародителя по имени может... гмм.. привести к обращению к прародителю соседнего объекта. Это именно то, о чем GunSmoker писал в статье - глюки при вызове FreeAndNil указывают на некорректности в Вашем коде.

    ОтветитьУдалить
  6. > На самом деле вызов объекта-прародителя по имени может... гмм.. привести к обращению к прародителю соседнего объекта.
    Читал-перечитывал, но честно говоря так не понял кто такой прародитель соседнего объекта. =)

    > Это именно то, о чем GunSmoker писал в статье - глюки при вызове FreeAndNil указывают на некорректности в Вашем коде.
    Я знаю, что код некорректный. Этот пост и писался как иллюстрация к посту GunSmoker-a. =)

    ОтветитьУдалить

Постоянные читатели