В основе CA-Visual Objects (КаВо) лежит Win32 API, написанный на языке Си. Поэтому с памятью компьютера он работает аналогичным образом. Создавая текст программ, мы используем переменные как фиксированной (заранее известной), так и перманентной длины (меняющейся по ходу исполнения приложения). Для затравки, первый тип – это целочисленные и натуральные, переменные типа «Date», «Logic». А второй – «String», «Array» и др… Классификация задана, остальное – «расфасуете» сами. :)
Обычно, переменные первого типа (фиксированной длины) не создают проблем, а вот второй… Имея полную информацию, «владелец» переменной может работать с ней как угодно (в т.ч. – «обнулять», очищая память). Но, если при закрытии, он не «убирается», или потеряет такую возможность (крах объекта) – уборщик ОС, не зная точных параметров задания, может сбойнуть. Память начинает «течь» (занятый объём всё время растёт), а программа - «вылетать».
Выход из ситуации – помочь системе очистки Windows. Пойдём по порядку. Все объекты программы описываются классами, у которых есть свой явный (метод «Init») или неявный конструктор. Значит, для решения проблемы с очисткой памяти – надо сделать деструктор, в котором прописать обнуление переменных. (Для простоты, достаточно сбросить только переменные «плавающей» длины). В КаВо его обычно зовут «Destroy»:
CLASS Test
PROTECT cText AS STRING
METHOD Destroy() CLASS Test
SELF:cText := NULL_STRING
RETURN NIL
Если мы хотим самостоятельно управлять процессом закрытия объекта с правильным освобождением памяти от используемых переменных – мы пишем свой деструктор.
Давайте разберём частности. Например, надо ли это делать в каждом классе? Нет. Очевидно что, если класс пуст (без переменных) – это пустая трата времени, и если содержит только «фиксированные» переменные – тоже можно ничего не делать. А если у вышеуказанного класса вдруг появится «наследник», как поступать с ним? Если он содержит свои собственные, помимо «родителя», динамические переменные, то для него «Destroy» лучше написать:
CLASS Y INHERIT Test
PROTECT auArray AS ARRAY
METHOD Destroy() CLASS Y
SELF:auArray := NULL_ARRAY
RETURN ( SUPER:Destroy() )
А можно записать вот так?
METHOD Destroy() CLASS Y
SUPER:Destroy()
SELF:auArray := NULL_ARRAY
RETURN NIL
Можно, но мне первый вариант нравится больше.
А можно сократить код и очищать таким способом?
METHOD Destroy() CLASS Y
SELF:auArray := NULL_ARRAY
RETURN NIL
Можно, но так вы разорвёте наследственную цепочку. А это значит, что переменные «родителя» могут не обнулиться. И тут придётся как-то выкручиваться …
А если у «папаши» не проводится полная очистка, как, например, здесь:
CLASS Test
PROTECT cText AS STRING
PROTECT uValue AS USUAL
METHOD Destroy() CLASS Test
SELF:cText := NULL_STRING
RETURN NIL
Можно ли это как-то поправить?
Можно. Само правильно – исправить код «родителя» (если у вас есть такая возможность). Но можно поработать и с «сыном»:
CLASS Y INHERIT Test
PROTECT auArray AS ARRAY
METHOD Destroy() CLASS Y
SELF:auArray := NULL_ARRAY
SELF:uValue := NIL
RETURN ( SUPER:Destroy() )
В этом случае (если правка сделана только у «сына») имеет смысл использовать только «наследника», но не «родителя». Причина, думаю, понятна.
- Ага, - скажет внимательный читатель, - а что делать, если объект внезапно закрылся или мы не сможем применить метод «Destroy»? Да, такая проблема есть. Чтобы её решить, в CA-Visual Objects придумали регистрацию объектов. Идея безумно проста: при запуске приложения, вместе с ним – запускать собственную систему очистки КаВо. И тогда, для наших «клиентов», эта «прачечная» сможет работать, «как часы».
Регистрация объекта обычно делается в методе Init():
CLASS Test
METHOD Init() CLASS Test
RegisterAxit( SELF )
RETURN SELF
В нужный момент, система проверит, зарегистрирована ли у неё ссылка на этот объект (есть ли у него метод «Axit») и при её наличии, передаст в него вызов. Получив его, мы сможем позаботиться о себе даже в аварийной ситуации. А чтобы не умножать сущности (и не писать же один и тот же код обнуления переменных в «Axit» и «Destroy»), лучше сделать объединение:
METHOD Axit() CLASS Test
IF ! InCollect() // Если мы ещё в «мусорном» списке
SELF:Destroy() // Идём на уборку в Destroy()
UnRegisterAxit( SELF ) // Удаляем регистрацию
ENDIF
RETURN NIL
Здесь можно сделать маленькую правку для случая, если мы хотим оставить «лазейку» (дающую возможность использовать Axit() по своему усмотрению или при отсутствии регистрации):
METHOD Axit() CLASS Test
IF ! InCollect() // Если мы ещё в «мусорном» списке
UnRegisterAxit( SELF ) // Удаляем регистрацию
ENDIF
SELF:Destroy() // Переходим к Destroy()
RETURN NIL
Примечание. Для объектов без регистрации - Axit() автоматически не сработает. Если выполнена регистрация, её снятие – обязательно. Для этого не обязательно использовать Axit(), можно «отписаться» и в Destroy().
Продолжим. Давайте ускорим мой пример:
METHOD Axit() CLASS Test
SELF:Destroy() // Переходим к Destroy()
RETURN NIL
METHOD Destroy() CLASS Test
// <Здесь что-то чистим>
IF ! InCollect() // Если мы ещё в «мусорном» списке
UnRegisterAxit( SELF ) // Удаляем регистрацию
ENDIF
RETURN NIL
Т.е., я предлагаю снимать регистрацию непосредственно в методе, выполняющем очистку. Приятная мелочь такого подхода: если мы завершим работу с классом через «Destroy», он автоматически снимет регистрацию, а встроенной системе «ассенизации» не придётся обращаться к Axit() с просьбой снять свою заявку на «клининговые услуги». (Пример – реализация классов «Control» и «VObject» в SDK).
Как думаете, надо ли регистрировать и классы-«потомки»? Можно, но не нужно. Зачем плодить записи? Если «отец» это уже сделал, она распространяется и на его «наследников». А снимать регистрацию в методе Axit() «потомка» надо? Подумайте сами... Есть «золотое» правило, звучащее в моей вольной трактовке как: «кто регистрацию сделал, тот её и снять должен».
CLASS Y INHERIT Test
PROTECT cText AS STRING
METHOD Axit() CLASS Y
SELF:cText := NULL_STRING
RETURN ( SUPER:Axit() )
Тот же пример, но для случая, если у «родителя» в методе Axit() делается вызов Destroy(). В таком случае, в классе «Y», вместо Axit() правильнее использовать Destroy():
CLASS Y INHERIT Test
PROTECT cText AS STRING
METHOD Destroy() CLASS Y
SELF:cText := NULL_STRING
RETURN ( SUPER:Destroy() )
Хорошо. Если в «родительском» Axit() написать вызов Destroy(), надо ли аналогично сделать и в Axit() «наследника»? Нет. Это же породит лишнюю цепочку событий: сначала пройдёт обработка Destroy(), инициализированная «наследником», а потом – ещё раз, запущенная «родителем». Поэтому, если уж написали вызов Destroy() в Axit() «родителя» – делать тоже самое у «потомка» – не надо.
Как узнать, а подключён ли класс-«родитель» к системе очистки? Абсолютно точный ответ даст только исходный текст. Но, можно проявить интуицию - поискать метод Axit() (у него и его родителей): если он есть, то, скорее всего, объект подключён к системе очистки. Ещё точнее даст ответ – эксперимент: сделать класс-«наследник», и в его методах поставить точки останова (AltD), проверив ситуации в отладке. Так же, может помочь функция Memory( MEMORY_REGISTERAXIT ) – она возвращает число зарегистрированных объектов (см. её описание в Help-е).
Допустим, у родителя уже есть и Axit() и Destroy(), но один из другого не вызывается, а я хочу… Можно исправить? Можно. Лучше – в исходном коде «родителя». При отсутствии возможности – сделать класс–«наследник», и в своих проектах (чтобы ваше «кольцо» работало) использовать только этот новый класс.
Подведём первые итоги:
- В используемых классах (объектах) желательно следить и вовремя «обнулять» переменные неопределённой длины (USUAL, STRING, ARRAY и т.п.).
- Естественно, это не касается классов без переменных или с переменными фиксированной длины.
- При создании собственного класса, надо принять решение: создавать его самостоятельно «с нуля» или на основе уже существующего. Рекомендую второй вариант – на основе КаВо-класса: контрол – на основе контрола, окно – на основе окна и т.д., т.к. это решит большинство вопросов.
- Если создавать класс «с нуля», в качестве основы лучше взять VObject{}, тогда вам останется только вопрос с регистрацией.
- А если вы решите сделать свой класс, который ни на что не опирается (не будет ни чьим «наследником»), то делать это надо внимательно, чётко понимая и прописывая все методы и их связи. (О чём я собственно и пишу).
Давайте потренируемся наводить порядок. Для примера возьмём класс DBServer. Что мы имеем:
- RegisterAxit() делается в Init().
- Вместо Destroy() используется приятный на слух Close() с UnRegisterAxit().
- Метод Axit() - есть, но в нём нет перехода в Close() и не снимается регистрация.
- Полностью переменные не чистятся ни в Axit() ни в Close()…
Я первый это заметил? Нет, конечно. Например, в библиотеке «
bBrowser» создан свой класс – «bDBServer», в котором решается часть проблем с использованием дополнительного метода Destruct
(). Но, мне это не нравится, и я предлагаю пойти прямым и простым способом:
CLASS msDBServer INHERIT DBServer
METHOD Axit() CLASS msDBServer
SELF:Close()
RETURN ( SUPER:Axit() )
METHOD Close() CLASS msDBServer
LOCAL lRet AS LOGIC
lRet := LOGIC( _CAST, SUPER:Close() )
// Чистим остатки от родителя (DataServer):
SELF:oHyperLabel := NULL_OBJECT
SELF:oHLStatus := NULL_OBJECT
// Чистим остатки от родителя (DBServer):
SELF:cRDDName := NULL_STRING
SELF:oFileSpec := NULL_OBJECT
SELF:aRelationChildren := NULL_ARRAY // Чистится в DBServer:Close(), но могут быть нюансы…
SELF:cbSelectionParentExpression := NIL
SELF:uSelectionValue := NIL
SELF:cbSelectionIndexingExpression := NIL
SELF:cbStoredForBlock := NIL
SELF:cbStoredWhileBlock := NIL
SELF:uStoredScope := NIL
SELF:lStoredAllRecords := NIL
SELF:oErrorInfo := NIL
SELF:aRdds := NULL_ARRAY
< А тут уже чистим свои переменные (если они есть) >
RETURN lRet
Из-за особенности закрытия в DBServer-а я применил небольшую «инверсию» - сделал SUPER:Close() не в конце, а в начале, сохранив полученное значение. Важно чтобы по окончанию работы с сервером всегда выполнялся Close(), ведь там снимается регистрация. И мы это сделали!
Тут кое-кто пробурчит, что было бы неплохо, если после закрытия всё-таки оставались какие-то переменные (типа, статуса и ошибки). Чтобы можно было что-то проверить или перепроверить, использовать… Дело - ваше. Тогда в Close() нельзя чистить эти данные. Придётся их «обнулять» как-то по-другому…
Тут есть тонкость: в DBServer переменная с данными об ошибке (oErrorInfo) не имеет регистрации и метода-«деструктора». Поэтому, вам придётся решать этот вопрос. Можно сделать на её основе класс-«наследник», заменив им в сервере стандартный DBError (oErrorInfo), что может вылиться в переписывание кода… А можно сделать «ход конём» - подключить ваш новый Error-класс в качестве дополнительной переменной, и в методе Close(), перед сбросом oErrorInfo, сохранять её значение в вашу новую переменную (которая подключена к системе очистки).
Давайте продолжим теорию. Мы уже разобрались с методами-деструкторами, поговорили о способе решения проблемы с «обнулением» при внезапном разрушении объекта. Но это ещё не всё, этого – недостаточно. Многие объекты событийно-зависимые, т.е., их поведение может управляться некими «толчками» (нажатием клавиш клавиатуры, мышки, запросов от других программ и пр.), что выражается в виде поступлений сигналов от других объектов.
Для этого в КаВо применяется обработчик (перехватчик) событий, который направляет их в нужное русло, в метод(ы) класса(ов). Как это сделано – хорошо видно в SDK. Обратите внимание, на классы окон.
// Это кусочек текста из Dispatch():
CASE (uMsg == WM_CLOSE)
IF SELF:QueryClose(oEvt)
SELF:__Close(oEvt)
ENDIF
RETURN SELF:EventReturnValue
METHOD __Close(oEvent AS @@Event) AS VOID PASCAL CLASS Window
SELF:Close(oEvent)
SELF:Destroy()
RETURN
По событию «WM_CLOSE» (закрытие) вызывается метод __Close(), который запускает последовательность: Close() и Destroy(). Т.е., закрытие по событию (в нормальной ситуации) и здесь приводит к Destroy(). «Дедушкой» класса окна является VObject, поэтому о «закольцовке» Axit() в Destroy() тоже заморачиваться не придётся.
Всё уже сделано до нас! Остаётся только правильно пользоваться методом Destroy().
И тут пытливый разум задаст сногсшибательный вопрос: «а надо ли в методе Destroy() окна обнулять переменные контролов?» Давайте посмотрим SDK и подумаем… Контролы сделаны на основе VObject, плюс правильно прописан Axit(). Значит, они «аннигилируют» самостоятельно и без нашего вмешательства! Ответ: можно не писать.
Если можно «не писать», то зачем всё это? А «это всё» полезно знать при создании своих классов или исправлении существующих!
Жду ваших отзывов!
23.02.2016 г. Карандаш.
P.S.: Рецензентом статьи (по моей просьбе) выступил наш старый знакомый – leo_bond. В дополнении к сказанному, он рекомендует использовать _RegisterExit() (описание см. в Help-е КаВо). Она предназначена для вызова функции очистки программы, что весьма полезно. В этой вызываемой функции можно, кроме стандартного обнуления глобальных переменных, включить анализ памяти, и узнать какие переменные «не ушли» из памяти, где и как они созданы.
Код его анализа я решил не прилагать к данной статье, т.к. теперь это стало стандартным примером использования функции CreateGCDump() в справке КаВо. Там вы его и найдёте.