Virtual — Директива Delphi


Virtual — Директива Delphi

Методы, с которыми вы имели дело в предыдущих разделах, называются статическими. По умолчанию все методы класса именно статические. Если в классе-наследнике переопределить такой метод (ввести новый метод с тем же именем), то для объектов этого класса новый метод отменит родительский. Если обращаться к объекту этого класса, то вызываться будет новый метод. Но если обратиться к объекту как к объекту родительского класса, то вызываться будет метод родителя.

Все это мы видели на примерах в предыдущем разделе. Но представим себе такую задачу. Вы объявили в приложении массив объектов типа TPerson:
var PersArray: array[1..10] of TPerson;

Далее заполняете этот массив вперемешку объектами классов TStudent и TEmpl, т.е. создаете, например, общий список учащихся и преподавателей. В разд. 3.5.4 вы видели, что это возможно, так как переменная базового класса может принимать объекты производных классов. А затем хотите пройти в цикле элементы этого массива и отобразить в окне Memo информацию о них:

for i:=l to 10 do Memol.Lines.Add(PersArray[i].PersonToStr);

Или аналогичная задача с использованием списка TList:
объявлена переменная var List: TList; в нее заносятся указатели на объекты классов TStudent и TEmpl, а затем вы хотите пройти в цикле элементы этого списка и отобразить их в окне Memo:

for i:=0 to List.Count — 1 do Memol.Lines.Add(TPerson(List[i]).PersonToStr);

Реализуйте один из этих вариантов (не забудьте при завершении приложения удалять объекты из памяти), и посмотрите, что получится. А получится очевидный результат: во всех случаях сработает метод PersonToStr родительского класса TPerson, так как соответствующие объекты объявлены объектами этого класса. И программа, естественно, не знает, объекты какого класса хранятся в массиве или в списке.

Конечно, можно усложнить код, проверять каждый раз операцией is истинный класс объекта, и указывать операцией as этот класс (см. об этих операциях в разд. 3.5.4). Для массива это будет выглядеть так:

А для списка аналогичный код имеет вид:

Задача будет решена, но некрасиво. Во-первых, код усложняется. А во-вторых, если вы впоследствии решите создать какие-то новые классы, производные от TPerson, вам придется вводить соответствующие дополнительные проверки в эти коды.

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

будут автоматически вызывать методы PersonToStr того класса (TStudent, TEmpl или других производных от TPerson), к которому относится каждый объект. Такой подход, облегчающий работу с множеством родственных объектов, называется полиморфизмом.

Сделать метод родительского класса виртуальным очень просто. При объявлении в классе виртуальных методов после точки с занятой, завершающей объявление метода, добавляется ключевое слово virtual. Например, чтобы объявить в базовом классе TPerson метод PersonToStr виртуальным, надо в его объявление добавить слово virtual:

function PersonToStr: string; virtual;

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

function PersonToStr: string; override;
И это все! Методы стали виртуальными и приведенные в начале данного раздела операторы, будут работать безо всяких проверок if.

Если в каком-то базовом классе метод был объявлен как виртуальный, то он остается виртуальным во всех классах-наследниках (в частности, и в наследниках классов наследников). Однако обычно для облегчения понимания кодов, перегруженные методы принято повторно объявлять виртуальными, чтобы была ясна их суть для тех, кто будет строить наследников данного класса. Например:

function PersonToStr: string; overr > В родительском классе виртуальный метод не обязательно должен быть реализован. Такой виртуальный метод, реализация которого не определена в том классе, в котором он объявлен, называется абстрактным. Предполагается, что этот метод будет перегружен в классах-наследниках. Только в тех классах, в которых он перегружен, его и можно вызывать.

Объявляется абстрактный метод с помощью ключевого слова abstract после слова virtual. Например, вы можете в классе TPerson объявить метод PersonToStr следующим образом:

function PersonToStr: string; virtual; abstract;

В этом случае вы должны удалить в этом классе реализацию метода. Класс, содержащий абстрактный метод, сам становится абстрактным. Объекты такого класса создавать нельзя. Можно создавать объекты только тех классов-наследников, в которых абстрактный метод перегружен и реализован.

Удачи!
Встретимся в следующем уроке!

Источник: www.thedelphi.ru
Автор: Савельев Александр
Опубликовано: 14 Июля 2013
Просмотров:

Зарегистрируйтесь или авторизуйтесь, чтобы добавлять комментарии.

Заметки о Pascal, Delphi и Lazarus

Следует ожидать переводов разделов справочной системы Delphi, компиляций из учебников, переводы статей, «путевые заметки» и прочие интересности. Блог прежде всего ориентирован на студентов, но опытных людей я тоже буду рад видеть;-)

среда, 2 мая 2012 г.

Методы

Метод – это процедура или функция, связанная с классом. При вызове метода определяется объект (или, если это метод класса, то класс) с которым должен работать метод. Например, SomeObject.Free вызывает метод Free объекта SomeObject.

О методах

Внутри объявления класса методы представлены заголовками процедур или функций, которые работают как упреждающие объявления. Где-то после объявления класса (но в этом же модуле) каждый метод должен быть реализован добавлением определяющего объявления. Например, предположим, что объявление TMyClass включает метод с именем DoSomething:

Определяющее объявление для DoSomething должно быть включено ниже в этом же модуле:

Класс может быть объявлен в модуле как в секции interface, так и в секции implementation, но определяющие объявления методов должны быть расположены в секции implementation.

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

Объявления методов могут включать специальные директивы, которые не используются при объявлении прочих процедур и функций. Директивы должны указываться только в объявлении класса, но не в определяющем объявлении, и могут следовать только в следующем порядке: reintroduce; overload; binding; calling convention; abstract; warning, где:

  • binding может принимать значения: virtual, dynamic или override;
  • calling convention может принимать значения: register, pascal, cdecl, stdcall или safecall;
  • warning может принимать значения: platform, deprecated или library.

Ключевое слово Inherited

Ключевое слово inherited играет особую роль при реализации полиморфизма в поведении методов. Оно может встречаться в объявлении методов, причем иногда с указанимем идентификатора после него, а иногда – без.

Если за ключевым словом inherited следует имя компонента, оно обозначает обычный вызов метода, обращение к свойству или полю, за исключением того, что поиск компонента, к которому идет обращение, начинается с непосредственного предка класса, к которому относится компонент. Например, если в определяющем объявлении метода присутствует:

происходит вызов наследуемого конструктора Create.

Когда за ключевым словом inherited не следует идентификатора, обращение идет к методу с таким же именем, как вызывающий метод, или (в том случае, когда вызывающий метод является обработчиком сообщений) к наследуемому обработчику для этого же сообщения. В этом случае вызов наследуемого метода не принимает явных параметров, но при этом выполняется неявная передача параметров из вызывающего метода. Например, вызов:

встречается часто в реализации конструкторов. Вызывает наследуемый конструктор с таким же списком параметров (передаваемых наследуемому конструктору).

Идентификатор Self

При реализации метода идентификатор Self обращается к объекту, из которого происходит вызов метода. Для примера приведем реализацию метода Add класса TCollection, объявленного в модуле Classes:

Метод Add вызывает метод Create класса, на который ссылается поле FitemClass (который всегда является наследником TCollectionItem). TCollectionItem.Create принимает единственный параметр типа TСollection. Таким образом, метод Add передает экземпляр объекта типа TCollection, вызывающего его. Это аналогично коду:

Self полезен по самым различным причинам. Например, если идентификатор компонента объявлен в классе, он может быть объявлен повторно в блоке метода, относящегося к этому классу. В этом случае, вы можете получить доступ к идентификатору компонента, используя вызов Self.Identifier.

Связывание методов

Связывание методов может быть статическим (static) (по умолчанию), виртуальным (virtual) или динамическим (dynamic). Виртуальные и динамические методы могут перекрываться и они могут быть абстрактными. Это подразделение методов начинает иметь значение, когда переменная одного типа класса хранит значение типа класса-потомка. Оно определяет, какая реализация метода будет активирована при запуске метода.

Статические методы

Методы по умолчанию являются статическими. При вызове статического метода, объявленный при компиляции тип класса или объектная переменная, использованные при вызове метода определяют, какая из реализаций метода активируется. В следующем примере метод Draw является статическим:

При таком объявлении приведенный ниже код иллюстрирует вызов статического метода. Во втором вызове Figure.Draw, переменная Figure ссылается на класс TRectangle, но вызов метода активирует реализацию метода Draw в классе TFigure, поскольку объявленный тип переменной Figure – это TFigure:

Виртуальные и динамические методы

Для того, чтобы сделать метод виртуальным или динамическим необходимо включить в его объявление соответствующую директиву (virtual или dynamic). Виртуальные и динамические методы, в отличии от статических могут быть перекрыты в классе-предке. При вызове перекрытого метода, решение о том, какую реализацию метода активировать, принимает действительный (определенный в процессе работы программы) тип класса или объекта, использванный при вызове метода.

Чтобы перекрыть метод его следует объявить повторно с указанием директивы override. Перекрывающее объявление должно совпадать с объявлением в классе-предке по порядку и типу параметров, а так же типу возвращаемого результата (для функций). В следующем примере метод Draw, объявленный в классе TFigure перекрывается в двух классах предках:

При таком объявлении нижеприведенный код показывает результат вызова виртуального метода при помощи переменной, чей тип меняется в процессе выполнения программы:

Только виртуальные и динамические методы могут быть перекрыты. Тем не менее, любые методы могут быть перегружены. Компилятор Delphi так же поддерживает концепцию финального виртуального метода. Если при объявлении виртуального метода было использовано ключевое слово final, классы-предки не смогут перекрывать его. Применение ключевого слова final может быть хорошим инженерным решением, которое может помочь документировать планируемое применение класса. Кроме того, оно помогает компилятору оптимизировать код.

Виртуальный или динамический?

В Delphi для Win32, виртуальные и динамические методы семантически одинаковы. Тем не менее, они отличаются в реализации обработки вызовов методов при выполнении программы. Виртуальные методы оптимизированы с точки зрения скорости, а динамические — снижают размер результирующего кода.

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

Замечание: Динамические методы следует использовать только в тех случаях, когда выгода от их применения очевидна. Во всех обычных ситуациях следует использовать виртуальные методы.

Скрывать или перекрывать?

Если объявление метода определяет такой же идентификатор и набор параметров, какой уже присутствует в объявлении метода в классе-предке, но при этом не использует директиву override, новое объявление просто скрывает наследуемый метод, не перекрывая его. Оба метода будут существовать в классе-предке, в котором имя метода связано статически. Например:

Директива Reintroduce

Директива reintroduce подавляет предупреждения компилятора о сокрытии объявленных ранее виртуальных методов. Например:

Директиву reintroduce следует использовать, когда вы хотите скрыть наследуемый виртуальный метод, заменив его новым методом.

Абстрактные методы

Абстрактные методы – это виртуальные или динамические методы, не имеющие реализации в том классе, где они объявлены. Они должны быть реализованы в классе-потомке. Абстрактные методы должны объявляться с использованием директивы abstract, следующей за директивой virtual или dynamic. Например:

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

Методы класса

Большая часть методов называются методами экземпляров, поскольку они работают с отдельными экземплярами объекта. Метод класса – это метод (отличный от конструктора), который оперирует классами, но не объектами. Существует два типа методов класса: обычные и статические.

Обычные методы класса

Объявление метода класса должно начинаться с зарезервированного слова class. Например:

Определяющее объявление метода класса также начинается с зарезервированного слова class:

В определяющем объявлении метода класса идентификатор Self представляет класс, в котором вызывается метод (может быть классом-потомком, по отношению к классу, в котором объявлен метод). Если методы вызывается в классе С, Self имеет тип class of C.

Таким образом, вы не сможете использовать Self для доступа к полям, свойствам или обычным методам экземпляра. Вы можете использовать Self для вызова конструкторов, прочих методов класса или для доступа к свойствам или полям класса. Метод класса может быть вызван при помощи ссылки на класс или объект. При вызове через ссылку на объект, класс объекта принимает значение Self.

Статические методы класса

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

Для объявления статического метода класса следует добавить ключевое слово класса к объявлению:

Как и обычные методы класса, статические методы могут вызываться при помощи типа класса (без ссылки на экземпляр класса):

Перегрузка методов

Метод может быть объявлен повторно с указанием директивы overload. В этом случае объявленный повторно метод имеет набор параметров, отличный от объявленного в классе-предке, и перегружает наследуемый метод, не скрывая его. Вызов метод в классе-потомке активирует реализацию метода с параметрами, совпадающими с фактическими.

Если вы перегружаете виртуальный метод, — пользуйтесь директивой reintroduce:

Внутри класса вы не можете установить видимость перегружаемых методов с одинаковыми именами в published. Обработка информации о типах в режиме выполнения программы требует уникального имени для компонентов с такой спецификацией видимости.

Методы, служащие для спецификации доступа к значению свойств (read или write) не могут быть перегружены. Реализация перегружаемых методов должна повторять список параметров, содержащихся в объявлении класса.

Конструкторы

Конструктор – это особый метод, который создает и инициализирует объекты. Объявление конструктора выглядит как объявление обычной процедуры, но начинается с ключевого слова constructor. Примеры:

Конструкторы должны использовать конвенцию вызова register. При том, что в объявлении конструктора не указывается возвращаемого значения, конструктор возвращает ссылку на создаваемый или вызывающий его объект.

Класс, может иметь более, чем один конструктор, но большая часть классов имеет только один. Обычно для конструкторов используется имя Create.

Для создания объекта следует вызывать конструктор, специфицируя название типа класса. Например:

Эта инструкция выделяет память для нового объекта, устанавливает значения для всех целочисленных полей в 0, присваивает значение nil для всех полей указательного типа и устанавливает пустое значение для всех строковых полей. Прочие операции, указанные в реализации конструктора, выполняются позже. Обычно объекты инициализируются значениями, которые передаются как параметры в конструктор. Наконец, конструктор возвращает ссылку на только что созданный и инициализированный объект. Тип возвращаемого значения совпадает с типом, указанным при вызове конструктора.

Если при выполнении конструктора, вызванного инструкцией с указанием ссылки на класс, возникает исключение деструктор Destroy вызывается автоматически для разрушения незаконченного объекта.

Когда конструктор вызывается инструкцией с указанием ссылки на объект, он не создает объект, а работает с вызывающим его объектом, выполняя только инструкции, указанные в его реализации, а затем возвращает ссылку на объект. Конструктор обычно вызывается инструкцией со ссылкой на объект в сочетании с зарезервированным словом inherited для выполнения наследуемого конструктора.

Далее приведен пример класса и его конструктора:

Первой инструкцией в конструкторе обычно бывает вызов наследуемого конструктора для инициализации унаследованных полей. Затем конструктор инициализирует поля, объявленные в классе-потомке. Поскольку конструктор всегда очищает память, которую он выделяет для создания объекта, все поля порядковых типов принимают значение 0, указатели и переменные типа класс – nil, строковые типы инициализируются пустым значением, а вариантные типы – значением Unassigned. Таким образом нет необходимости в теле конструктора инициализировать поля значениями, отличными от ноля или пустых значений.

При вызове через идентификатор типа класса конструктор, объявленный как virtual, ведет себя так же как обычный статический конструктор. Тем не менее, при вызове через ссылку на класс виртуальные конструкторы допускают полиморфное создание объектов – то есть создание объектов, тип которых неизвестен на этапе компиляции.

Деструкторы

Деструктор – это особый метод, который при вызове разрушает объект и освобождает занимаемую им память. Объявление деструктора выглядит как объявление обычной процедуры, но начинается с ключевого слова destructor. Например:

Деструкторы на платформе Win32 используют по умолчанию конвенцию вызова register. Хоть класс и может иметь несколько деструкторов, рекомендуется перекрывать наследуемый метод Destroy, без объявления прочих деструкторов.

Для вызова деструктора вы должны специфицировать экземпляр объекта:

При вызове деструктора вначале выполняются инструкции, указанные в его теле. Обычно это уничтожение встроенных объектов и высвобождение ресурсов, выделенных для разрушаемого объекта. После чего память, выделенная для объекта освобождается.

Далее приведен пример реализации деструктора:

Последней инструкцией в теле деструктора обычно бывает вызов наследуемого деструктора, которые уничтожает наследуемые поля. При возникновении исключительной ситуации в процессе создании объекта, для разрушения незаконченного объекта автоматически вызывается деструктор Destroy. Это обозначает, что деструктор Destroy должен быть подготовлен к разрушению частично созданных объектов. Поскольку конструктор устанавливает нулевые и пустые значения полей перед выполнением каких-либо других действий, поля типа класс и указатели в частично созданном объекте всегда имеют значение nil. Прежде чем работать с такими полями деструктор должен проверять их на наличие этого значения. Вызов метода Free (объявлен в TObject) – это более подходящий способ для разрушения объекта с предварительной проверкой значений его полей.

Конструкторы классов

Конструктор класса – это особый метод класса, который недосупен разработчикам. Вызовы конструкторов автоматически вставляются компилятором в код секции инициализации модуля, в котором объявлен класс. Обычно конструкторы классов применяются для инициализации статических полей класса или для выполнения особого типа инициализации, требуемой для того, чтобы класс или какой-либо его экземпляр функционировал корректно. Даже если такой же результат может быть достигнут размещением кода инициализации класса в секции initialization, конструкторы класса в большей степени помогают компилятору решить, какой именно класс должен быть включен в результирующий двоичный файл, а какой – удален из него.

Следующий пример показывает обычную инициализацию полей класса:

Этот способ плох тем, что приложение включает модуль, объявляющий TBox, даже в том случае, если этот модуль на самом деле не используется им. В приведенном примере класс TBox включается в результирующий бинарный файл, поскольку он упоминается в секции инициализации. Чтобы исправить ситуацию, рассмотрим применение конструкторов класса:

В этом случае, компилятор проверяет, используется ли где-либо в приложении класс TBox, и, если это так, вызов конструктора класса автоматически добавляется в секцию инициализации модуля.

Замечание: Несмотря на то, что компилятор уделяет внимание порядку инициализации классов, в некоторых сложных ситуациях он может оказаться случайным. Это происходит, когда конструктор класса зависит от состояния другого класса, который, в свою очередь, зависит от первого.
Замечание: конструктор класса-дженерика или записи может быть выполнен несколько раз. Точное количество запусков конструктора зависит от количества специализированных версий типа-дженерика. Например, конструктор класса для специализированного класса TList&ltString&gt может быть выполнен несколько раз в одном и том же приложении.

Деструкторы классов

Деструкторы классов выполняют финализацию класса. Деструкторы класса обладают тем же преимуществом, что и конструкторы класса. Следующий пример построен на предыдущем (о конструкторах класса) и объявляет финализирующую подпрограмму:

Замечание: деструктор класса-дженерика или записи может быть выполнен несколько раз. Точное количество запусков деструктора зависит от количества специализированных версий типа-дженерика. Например, деструктор класса для специализированного класса TList&ltString&gt может быть выполнен несколько раз в одном и том же приложении.

Методы-обработчики сообщений

Обработчики сообщений реализуют реакцию на динамически передаваемые сообщения. Синтаксис метода-обработчика сообщения поддерживается для всех платформ. Приложения VCL применяют обработчики сообщений для реагирования на сообщения Windows.

Обработчик сообщения объявляется при помощи директивы message, за которой следует целочисленная константа с диапазоном значений от 1 до 49151, представляющая собой идентификатор сообщения. Для обработчиков сообщений в элементах управления VCL эта константа может быть идентификатором одного из сообщений Win32, определенных в модуле Messages. Метод-обработчик сообщения должен быть процедурой с единственным параметром, передаваемым по ссылке.

Обработчик сообщения не должен содержать директивы override для переопределения наследуемого метода. На самом деле он даже не должен повторять имени или типа параметра перекрываемого метода. Только идентификатор сообщения определяет, какое сообщение должен обработать метод и является ли он перекрывающим методом.

Реализация методов-обработчиков сообщений

Реализация обработчика может вызывать наследуемый метод-обработчик. Например:

Инструкция inherited выполняет поиск по иерархии класса и подключает первый найденный метод с соответствующим идентификатором сообщения и передает ему запись сообщения. Если классы-предки не реализуют метод для обработки сообщения с данным идентификатором, директива inherited вызывает метод DefaultHandler , объявленный в TObject.

Илон Маск рекомендует:  Что такое код assert_options

Реализация DefaultHandler в TObject не подразумевает выполнения каких-либо операций. Перекрывая DefaultHandler, класс может реализовать собственную обработку сообщений по умолчанию. Для элементов управления на платформе Win32, метод DefaultHandler вызывает Win32 API DefWindowProc.

Обработка сообщений

Обработчики сообщений редко вызываются явно. Обычно сообщения передаются объекту при помощи метода Dispatch , наследуемого от TObject:

Параметр Message передаваемый методу Dispatch должен быть записью, первым полем которой должно быть поле типа Word, содержащее идентификатор сообщения.

Dispatch выполняет поиск обработчика сообщения в иерархии классов (начиная с класса объекта, в котором был вызван этот метод) и запускает первый найденный обработчик для идентификатора, который был ему передан. Если обработчика для сообщения с данным идентификатором не находится, Dispatch вызывает DefaultHandler.

Virtual — Директива Delphi

Профессиональная разработка приложений с помощью Delphi5

Часть 3. Оформление приложений для Windows95/98/NT/2000 в Delphi

© Сергей Трепалин
УКЦ Interface Ltd.
КомпьютерПресс #3 2001
Статья была опубликована в КомпьютерПресс

Данная публикация является первой в цикле статей, посвященном профессиональной разработке приложений с помощью Delphi 5. Статьи цикла будут содержать базовые сведения, необходимые для создания и отладки приложений, отвечающих современным требованиям. Они представляют интерес для любого программиста — независимо от области применения создаваемых приложений.

Объектно-ориентированное программирование (ООП) – это основа для создания приложений в Windows. В 80-х годах материалов, посвященных объектно-ориентированному программированию, публиковалось довольно много. Несмотря на то что за прошедшее время в синтаксисе объектно-ориентированных языков программирования произошли существенные изменения и появились новые возможности, авторы современных учебных пособий почему-то не уделяют этому вопросу должного внимания. Мы решили восполнить этот пробел. В данной публикации в первую очередь рассматривается то новое, что появилось в Delphi по мере совершенствования этого продукта.

Материалы, изложенные в настоящем разделе (если это особо не оговаривается) применимы как к Delphi, так и к C++Builder (за исключением синтаксиса).

Современное название объекта – класс; термин «объект» иногда используется для обозначения рабочей копии (экземпляра) класса.

Класс можно вкратце определить как запись record, к которой добавлены методы для работы с переменными внутри этой записи. Однако в отличие от записей и методов, объявленных вне класса, класс обладает рядом дополнительных возможностей.

Каждому классу, за исключением самого первого, должен предшествовать класс-родитель. В свою очередь, любой класс можно использовать для создания других классов, и он в этом случае будет являться их родителем. В Delphi у класса бывает только один родитель, в C++ родителей может быть несколько. Поэтому в Delphi классы образуют иерархическое дерево с классом TObject в роли корня. Иерархию классов в Delphi можно проследить при вызове команды View/Browser после успешной компиляции какого-либо проекта.

В C++ такое дерево построить невозможно: отдельные его ветви будут сливаться.

Иерархическое дерево реализуется довольно просто: при определении в среде-разработке нового класса указывается класс-предок:

Разрешается не указывать класс-предок. В этом случае по умолчанию считается, что предком является TObject. Код вида

Такое объявление однозначно определяет место нового класса в иерархии классов. Кроме того, оно означает следующее: все переменные и все методы класса-предка копируются в новый класс. Простым объявлением TMyListBox=class(TListBox) мы получили новый класс, который обладает всеми свойствами списка: в него можно добавлять строки, он будет показан на форме, при необходимости на нем автоматически появится вертикальная полоса прокрутки и т.д. Таким образом, при продвижении по ветвям иерархического дерева происходит накопление переменных и методов. Например, класс TWinControl имеет все переменные и методы, определенные в классах TControl, TComponent, TPersistent и TObject.

Самый простой класс – TObject — не имеет предка. Он также не имеет полезных при обычном написании приложений методов и переменных. Однако он играет важнейшую роль в поведении объектов.

Объявление переменных и методов класса

Переменные в объекте используются для хранения данных, в качестве которых можно использовать любые типы из Object Pascal, в том числе и сами классы. Добавляются они в новый класс так же, как и в тело приложения, но без служебного слова var:

TMy > public
FName:string;
FAge:integer;
end;

В класс TMyClass к переменным класса TObject добавлены две новые переменные: FName и FAge. Названия переменных в классе принято (но не обязательно) начинать с буквы F от слова field. Классовые переменные, определенные внутри класса, отличаются от глобальных (служебное слово var в секции interface или implementation модуля) и локальных (служебное слово var в процедуре или функции) переменных. При загрузке приложения память для глобальных переменных выделяется немедленно и освобождается по завершении приложения. Локальным же переменным память выделяется в стеке при вызове метода, и после завершения работы метода эти ресурсы возвращаются системе.Так, если была объявлена, например, одна глобальная (или локальная) переменная типа N:integer, то резервируется 4 байта памяти, куда можно поместить одно значение. При объявлении же классовых переменных во время загрузки приложения не выделяется память для их хранения – она выделяется только при создании экземпляра класса после вызова конструктора (см. ниже). Поскольку экземпляров класса может быть несколько, в работающем приложении может быть несколько копий классовых переменных (в том числе и нулевое количество). Соответственно в каждой из этих переменных могут храниться различающиеся данные. Этим и определяется отличие классовых переменных от глобальных и локальных – для последних имеется только одна копия. Еще одной интересной особенностью классовых переменных является то, что при создании экземпляра класса они инициализируются нулями (то есть все их биты заполняются нулями). Поэтому если такая переменная представляет собой указатель, то он равен nil, если целое число, то оно равно 0, если логическую переменную, то она равна False. Локальные и глобальные переменные не инициализируются.

Формально объявление метода класса похоже на объявление обычного, не относящегося к классу, метода:

TMy > public
FName:string;
FAge:integer;
procedure DoSomething(K:integer);
function AddOne(N:integer):integer;
end;

В данном примере к методам TObject добавлены два новых метода – DoSomething и AddOne. Синтаксис Object Pascal разрешает объявлять новые методы только после объявления переменных – приведенный ниже пример вызовет ошибку компиляции:

TMy > public
FName:string;
procedure DoSomething(K:integer);
FAge:integer;
function AddOne(N:integer):integer;
end;

После объявления какого-либо метода в классе необходимо в секции implementation данного модуля описать его реализацию. Перед заголовком метода следует поместить указание на класс, к которому он относится. Это необходимо делать, поскольку различные классы могут иметь методы с одинаковыми названиями:


interface
type
TMy > procedure DoSomething(N:integer);
end;
TSecond > procedure DoSomething(N:integer);
end;


implementation
procedure TMyClass.DoSomething(N:integer);
begin

end;

procedure TSecondClass.DoSomething(N:integer);
begin

end;

Методы, объявленные в классе

Методы, описываемые в классе, подразделяются на классовые методы и другие методы. Приведем примеры их объявления и реализации:

TMy > class procedure DoSomething(K:integer);

function AddOne(N:integer):integer; absent – means method>
end;

implementation

class procedure TMyClass.DoSomething(K:integer);
begin

end;

function TMyClass.AddOne(N:integer):integer;
begin

end;

Обратите внимание, что и в секции реализации используется служебное слово class.

Классовые методы можно вызывать без создания экземпляра класса. Разновидность классовых методов – конструкторы – используются для создания рабочих копий (экземпляров) класса. О создании экземпляров класса будет сказано ниже, пока же достаточно знать, что может быть несколько (в том числе и ни одной) копий экземпляров класса. Классовый метод полностью характеризуется одним адресом (указателем) в памяти, в этом его отличие от других методов класса. Процессор просто передает управление по этому адресу при вызове соответствующего метода. В классовых методах (но не в конструкторах!) нельзя использовать классовые переменные: они становятся доступными только после создания экземпляра класса, а классовый метод можно вызывать, не создавая экземпляр.

Главное отличие обычных методов от классовых заключается в том, что они могут работать с классовыми переменными, то есть с переменными, которые определены в классе (см. выше). Эти методы характеризуются уже двумя адресами: адресом метода и адресом данных. Второй адрес необходим потому, что для обслуживания всех созданных экземпляров класса используется единственная копия метода в памяти. Методы загружаются сразу же после старта приложения. При создании каждого нового экземпляра данного класса он пользуется уже загруженными в память методами. При этом для работы с классовыми переменными каждому методу незаметно для программиста передается ссылка на данные, и таким образом достигается экономия ресурсов: при создании новых экземпляров класса память выделяется только для хранения переменных.

Для работы с методами имеется структура TMethod, определенная в модуле SysUtils:

type
TMethod = record
Code, Data: Pointer;
end;

Эта запись позволяет «разобрать» и вновь «собрать» метод класса на две переменные типа Pointer, что бывает полезным для передачи ссылки на метод во внутренний (in-process) сервер автоматизации.

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

TMy > function AddOne(N:integer):integer; – means
static method>
procedure Rotate(Angle:single); virtual;
procedure Move(Distance:single); dynamic; method>
end;

Эти методы различаются по способу загрузки в память при старте приложения и по способу их вызова. Для вызова статических методов используется его адрес в памяти компьютера – такой же способ, как и для обычных методов. Вызов же динамических и виртуальных методов происходит иначе. Первоначально процесс обращается к таблице виртуальных или динамических методов. Эти таблицы формируются для каждого класса при загрузке приложения. В таблице находится адрес соответствующего метода, по которому передается управление. При этом в таблицах можно подменить адрес метода, в результате чего приложение будет вызывать другой метод так же, как и ранее определенные методы. При вызове одних и тех же методов различных классов может выполняться различный код. Такое поведение классов называется полиморфизмом (об этом подробно рассказывается в следующей главе). Полиморфизм возможен только для динамических и виртуальных методов, но не для статических. Это главное отличие статических методов от остальных.

В классическом объектно-ориентированном программировании требуется наличие виртуальных методов. Динамические методы поддерживают не все объектно-ориентированные языки, но их использование является достаточно эффективным. Для понимания различий между ними рассмотрим создание главной формы какого-либо приложения. При этом разберемся, как в памяти компьютера распределяются три метода формы: DoEnter (динамический метод), CreateWnd (виртуальный) и DoKeyDown (статический). Каждый из этих методов определен на уровне TWinControl.

Иерархия классов, которая ведет от TWinControl к TForm1, следующая:

TWinControl -> TScrollingWinControl -> TCustomForm -> TForm ->
TForm1

При этом в памяти компьютера будет размещена единственная копия статического метода DoKeyDown. При создании экземпляра любого из вышеперечисленных классов и вызове из него метода DoKeyDown компилятор генерирует код, который ссылается на адрес этого метода. Таким образом, один экземпляр статического метода обслуживает не только несколько экземпляров класса, в котором он определен, но и все экземпляры его потомков. При этом следует обратить внимание на то, что вызов метода происходит очень быстро: в коде указывается его адрес в ОЗУ.

При динамическом методе DoEnter в памяти компьютера создается единственная его копия, а в таблице динамических методов TWinControl указывается его адрес. В таблицах динамических методов классов TScrollingWinControl…TForm1 в качестве адреса этого метода указывается nil. При вызове этого метода из экземпляра класса TForm1 первоначально происходит поиск этого метода в таблице динамических методов TForm1. Естественно, метод найден не будет, и поиск продолжится уже в таблице динамических методов класса TForm. Так будет продолжаться до тех пор, пока не начнется поиск в таблице динамических методов TWinControl, где будет найден его адрес, по которому будет передано управление процессом. Как и статические, динамические методы требуют мало ресурсов: один экземпляр динамического метода обслуживает как экземпляры класса, где он определен, так и экземпляры всех его потомков. Но вызов метода происходит достаточно долго, поскольку для этого приходится просматривать несколько таблиц. Вызов может замедляться еще и при использовании директивы компилятора <$R+>(проверка диапазона допустимых значений).

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

Таким образом, статические методы вызываются очень быстро, и для их хранения используется минимальное количество системных ресурсов. Но переписать их для того, чтобы заставить класс выполнять другой (или добавочный) код, невозможно. Динамические методы можно переписать, но их вызов требует достаточно длительного времени. Однако при этом они также потребляют мало системных ресурсов. И наконец, виртуальные методы можно переписать, вызов их производится с почти той же самой скоростью, что и вызов статических методов. Но ресурсов они используют достаточно много: число копий виртуальных методов пропорционально числу ветвей в дереве классов, умноженных на их длину. Именно по этой причине классы, имеющие многочисленных потомков, например TControl, практически не имеют виртуальных методов, а обходятся только динамическими

Переписанные методы. Полиморфизм. Абстрактные методы

Методы классов могут вызываться как непосредственно программистом, так и автоматически в ответ на какие-либо события. Предположим, перед вызовом метода необходимо выполнить какое-либо действие. Если метод вызывается в явном виде из кода программы, то программист может перед вызовом метода дописать необходимый код. Однако достаточно часто бывает необходимо выполнить действия перед автоматическим вызовом классом какого-либо метода в ответ на какое-либо событие (или после вызова метода). Например, класс TEdit (однострочный редактор текста) позволяет редактировать текст. При этом, например, программист хочет, чтобы пользователь не мог покинуть данный элемент управления, пока в нем не будет введено корректное значение какого-либо текста (например, целое число). Но покинуть данный элемент управления и перейти к другому пользователь может несколькими способами – щелкнуть мышкой на другом элементе, нажать акселератор или клавишу Tab. Если бы программист никак не мог изменить имеющиеся методы, он вынужден был бы перехватывать события о нажатой клавише мыши на всех имеющихся на форме элементах управления, проверять содержимое целевого элемента и при необходимости возвращать ему фокус ввода. Такие же проверки необходимо было бы сделать для обработчика событий, связанных с нажатием на клавиши…

К счастью, в классах виртуальные и динамические (но не статические!) методы можно подменить на другие, созданные программистом. При этом, если данный метод вызывается автоматически, будет выполняться уже новый метод, написанный программистом. Такая подмена осуществляется при использовании служебного слова override. В данном случае (проверка содержимого редактора перед выходом из него) решение будет заключаться в следующем. Любой объект класса TWinControl (и TEdit) вызывает метод DoExit перед тем, как он теряет фокус ввода. Этот метод является динамическим, и его можно переписать:

TMyEdit=class(TEdit)
protected
procedure DoExit; override;
end;

implementation

procedure TMyEdit.DoExit;
var
N,I:integer;
begin
inherited DoExit;
Val(Text,N,I);
if I<>0 then begin
MessageDlg(‘Illegal value’,mtError,[mbOK],0);
SetFocus;
end;
end;

Теперь, создавая копию данного класса в обработчике события OnCreate главной формы

procedure TForm1.FormCreate(Sender: TObject);
begin
with TMyEdit.Create(Self) do begin
Parent:=Self;
Left:=200;
Top:=100;
end;
end;

получаем однострочный редактор текста, который невозможно покинуть до тех пор, пока в него не будет введено целое число.

Таким образом, переписывание виртуального или динамического метода осуществляется следующим образом. В классе-потомке определяется метод с тем же самым названием и с тем же самым списком параметров, который был ранее объявлен в каком-либо из классов-предков данного класса. При этом не имеет значения, как называются формальные параметры переписываемого метода, значение имеет их порядок следования, модификаторы (var, const) и их тип. Например, на уровне TWinControl определен виртуальный метод: procedure AlignControls(AControl: TControl; var Rect: TRect); При его переписывании в каком-либо классе-потомке данный метод можно определить следующим образом:

procedure AlignControls(AC: TControl; var R: TRect); override;

Однако компилятор Delphi не пропустит перечисленные ниже определения:

procedure AControls(AControl: TControl; var Rect: TRect); override; is not consistent>
procedure AlignControls(AControl: TControl; Rect: TRect); override; missed>
procedure AlignControls(var Rect: TRect); override;

Следует обратить внимание на использованное в реализации метода DoExit служебное слово inherited. Подробно его значение мы обсудим позднее, пока же будем считать, что он вызывает метод класса-предка. При переписывании методов в большинстве случаев необходимо вызывать методы класса-предка или, по крайней мере, четко представлять себе, что случится, если не вызвать метод предка. Забывание оператора inherited, как правило, приводит к фатальным сбоям в приложении, причем часто это проявляется не сразу.

Итак, рассмотрим еще раз, что происходит в классе TEdit и его потомке TMyEdit. Класс TEdit устроен таким образом, что перед тем, как он теряет фокус ввода, приложение обращается к таблице динамических методов TEdit и извлекает оттуда адрес 27-го метода (метод DoExit среди динамических определен 27-м по счету). После этого управление процессом передается по найденному адресу. Класс-потомок TMyEdit имеет собственные таблицы виртуальных и динамических методов. Таблица динамических методов отличается тем, что в нем 27-й адрес уже указывает на реализованный нами метод DoExit. Соответственно приложением извлекается уже новый адрес, и управление процессом передается вновь реализованному методу. При его старте происходит обращение к таблице динамических методов класса TEdit и вызывается 27-й метод – это делает строка inherited DoExit. Затем проверяется содержимое свойства Text в экземпляре класса TMyEdit, и если это не целое число, то об этом сообщается пользователю и фокус ввода вновь переносится на редактор текста. Схематически это можно изобразить на рисунке следующим образом.

При забывании директивы override появляется новый, статический метод с тем же самым названием и списком параметров, что и ранее определенный виртуальный (или динамический) метод. При этом новый метод делает «невидимым» старый, то есть при явном вызове метода из кода приложения будет вызываться новый, определенный программистом метод. Если метод вызывается классом в ответ на какое-либо событие, по-прежнему будет вызываться старый метод (адрес в таблицах не изменится). Такое поведение объекта часто приводит к печальным последствиям, которые иногда очень трудно обнаруживаются.

С переписываемыми методами тесно связано и другое понятие для классов – полиморфизм, суть которого заключается в том, что родительский класс имеет какой-либо виртуальный или динамический метод, а в различных его потомках этот метод переписывается по-разному. После этого выполняя формально одни и те же методы, можно добиться принципиально разного результата . Хороший пример полиморфных классов – класс TStream (базовый) и три его потомка – TFileStream, TMemoryStream и TResourceStream. Эти классы используются для хранения и передачи данных в двоичном формате. В базовом классе TStream определено несколько абстрактных методов, например:

function Write(const Buffer; Count: Longint): Longint; virtual;

а в классах-потомках этот метод переписан таким образом, что записывает данные из переменной Buffer в файл (TFileStream), или в ОЗУ (TMemoryStream), или в ресурсы (TResourceStream). Программист при реализации приложения может определить метод для сохранения своих данных в двоичном формате:

procedure SaveToStream(Stream:TStream);
begin
Stream.Write(FYear,sizeof(FYear));
Stream.Write(FMonth,sizeof(FMonth));
<. A very long code to save all data as binary may
be inserted here …>
end;

Далее, вызывая этот метод и используя разные классы-потомки, можно сохранить данные либо в памяти компьютера, либо в файле:

procedure TForm1.Button1Click(Sender: TObject);
var
MStream:TMemoryStream;
FStream:TFileStream;
begin
MStream:=TMemoryStream.Create;
SaveToStream(MStream);
DoSomething(MStream);
MStream.Free;
FStream:=TFileStream.Create(‘C:\Test.dat’,fmCreate);
SaveToStream(FStream);
FStream.Free;
end;

В классах, являющихся базовыми для полиморфизма, часто используют абстрактные методы. Они объявляются со служебным словом abstract:

TMy > protected
procedure DisplayGraphic(Canvas:TCanvas); virtual;
abstract;
end;

При объявлении метода абстрактным реализация этих методов не требуется (более того – не допускается компилятором). В секции implementation для метода DisplayGraphic абсолютно ничего писать не надо. Для абстрактного метода в виртуальной (или динамической) таблице методов резервируется запись, куда помещается адрес nil. В классах-потомках на место этого адреса подставляются реальные адреса.

При попытке вызвать абстрактный метод возникает исключение – метод-то отсутствует! В частности, для вышеприведенного примера метод Write класса TStream является абстрактным и при попытке вызвать метод SaveToStream

procedure TForm1.Button1Click(Sender: TObject);
var
Stream:TStream;
begin
Stream:=TStream.Create;
SaveToStream(Stream);
Stream.Free;
end;

Классы, содержащие абстрактные методы, называют абстрактными; они являются базовыми для создания классов-потомков. В любом случае не следует создавать экземпляры абстрактных классов в приложении! Компилятор Delphi тем не менее позволяет осуществлять вызов конструкторов абстрактных классов с соответствующим предупреждением.

Так же как и для обычных методов, в методах класса допустима директива overload – перегружаемый метод:

TOver1 > public
procedure DoSmth(N:integer); overload;
class procedure DoSecond(N:integer); overload; dynamic;
end;

Перегружаемые методы могут быть как классовыми, так и обычными. Они способны быть статическими, динамическими и виртуальными. Следующие перегружаемые методы могут быть объявлены как в исходном классе, так и в классах-потомках:

TOver2 > public
procedure DoSmth(S:string); overload;
class procedure DoSecond(S:string); reintroduce; overload; dynamic;
end;

Теперь можно вызывать методы DoSmth и DoSecond из экземпляра TOver2Class с целочисленным и строковым параметром:

procedure TForm1.Button3Click(Sender: TObject);
var
CO:TOver2Class;
begin
CO:=TOver2Class.Create;
CO.DoSmth(1);
CO.DoSmth(‘Test’);
CO.Free;
end;

В файлах помощи Delphi рекомендуют использовать директиву reintroduce, когда вводится перегружаемый виртуальный метод, но и без этой директивы приложение прекрасно работает. Директива reintroduce просто подавляет сообщение компилятора (warning) о возникновении скрытого метода.

Создание экземпляров классов. Конструкторы и деструкторы

Переменная типа класса объявляется в приложении так же, как обычная переменная:

При таком объявлении резервируется 4 байта памяти для любого класса. Очевидно, что этого явно недостаточно для хранения всех переменных в классе. Размер этой переменной говорит о том, что в ней хранится указатель. Экземпляр (или рабочая копия) класса создается посредством вызова его конструктора:

При вызове конструктора класса резервируется память, необходимая для хранения данных в объекте (переменных). Обращаться к переменным и методу класса можно только после вызова конструктора, иначе попытка их чтения/записи приводит к исключительной ситуации. Обращение к переменным и методам экземпляра класса осуществляется с указанием на экземпляр:

Илон Маск рекомендует:  Как в visual basic создать цикл

procedure TForm1.Button1Click(Sender: TObject);
var
SL1,SL2:TStringList;
begin
SL1:=TStringList.Create;
SL2:=TStringList.Create;
SL1.Add(‘String added to SL1 object’);
SL2.Add(‘String added to SL2 object’);
….
end;

Довольно распространенной ошибкой (которая, правда, тут же обнаруживается) является вызов конструктора с попыткой обратиться к несуществующему экземпляру класса:

procedure TForm1.Button1Click(Sender: TObject);
var
SL:TStringList;
begin
SL:=nil;
try
SL.Create; typed SL:=TStringList.Create;>

finally
if Assigned(SL) then SL.Free;
end;
end;

Данный код компилятор пропускает без каких-либо диагностик, но попытка выполнить его немедленно приводит к возникновению исключения.

Любой класс может иметь несколько конструкторов. Примером может служить класс Exception, имеющий восемь конструкторов. По соглашению имя конструктора содержит слово Create (CreateFmt, CreateFromFile…). Конструкторы могут быть как статическими, так и виртуальными или динамическими. Последние могут быть переписаны – в классах-потомках при необходимости определяется новый конструктор со служебным словом override. Переписывать конструкторы необходимо только для компонентов Delphi и для форм – во всех остальных классах их можно просто добавлять к существующим методам. Необходимость переписывания конструкторов компонентов и форм обусловлена тем, что их вызывает среда разработки. Забытая директива override в компоненте приводит к тому, что при создании формы не выполняется новый конструктор. В большинстве других классов (не потомков TComponent) конструктор вызывается в явном виде из приложения, и поэтому будет вызываться последний написанный конструктор.

Конструктор необходимо переписывать (или создавать новый), когда необходимо изначально (при создании экземпляра класса) изменить значения переменных, или запомнить в переменных параметры, передаваемые в конструкторе, или создать экземпляры классов, объявленных внутри другого класса:

TMyBox=class(TListBox)
private
FData:TList;
FNAccel:integer;
public
constructor Create(AOwner:TComponent); override;
end;
implementation
constructor TMyBox.Create;
begin
inherited Create(AOwner);
FData:=TList.Create;
FNAccel:=5;
Items.Add(‘1’);
end;

Следует обратить внимание на то, что в конструкторе первым вызывается inherited-метод – конструктор класса-предка и только потом пишется код для инициализации переменных. Это обязательное условие в объектно-ориентированном программировании, которое может нарушаться только в отдельных случаях (примером такого случая является класс TThread). При таком способе записи каждый конструктор предка будет вызывать конструктор своего предка – и так до уровня конструктора класса TObject, который фактически будет первым оператором при вызове конструктора любого класса. Далее происходит выполнение кода в конструкторе класса-потомка и т.д. Для класса TMyBox при обращении к конструктору сначала происходит резервирование памяти для хранения переменных, определенных в данном классе и его предках. Затем вызывается конструктор TObject. Далее происходит обращение к конструктору TComponent, который устанавливает связь экземпляра TMyBox с его владельцем, передаваемым в параметре AOwner. Выполняется код конструктора TCustomListBox, который создает экземпляр класса TStrings и инициализирует ряд переменных. И наконец выполняются операторы, определенные в конструкторе TMyBox. Если оператор inherited поставить последним в конструкторе TMyBox, произойдет исключение при выполнении оператора Items.Add(‘1’) – объект для хранения строк создается в конструкторе класса TCustomListBox, который еще не был вызван.

Понятно, что при создании экземпляра класса резервируются ресурсы операционной системы, и когда экземпляр становится ненужным, эти ресурсы необходимо вернуть обратно посредством вызова деструктора. Деструктор объявлен виртуальным на уровне класса TObject. Деструктор объявляется следующим образом:

TMyBox=class(TListBox)
private
FData:TList;
FNAccel:integer;
public
constructor Create(AOwner:TComponent); override;
destructor Destroy; override;
end;
implementation
destructor TMyBox.Destroy;
begin
FData.Free;
inherited Destroy;
end;

Так будет выглядеть новый деструктор, который мы обязаны объявить в примере, приведенном выше. При вызове деструктора следует ссылаться на конкретный экземпляр класса, который необходимо разрушить:

Для этого примера должен быть создан новый деструктор, так как внутри экземпляра класса TMyBox создается экземпляр класса TList. Соответственно разрушаться они должны совместно.

При переписывании деструктора прежде всего разрушаются экземпляры классов, созданных внутри данного класса, и только после этого вызывается деструктор класса-предка inherited Destroy (отметим, что в конструкторе используется обратный порядок). При таком способе вызова в последнюю очередь будет вызван метод Destroy класса TObject, который вернет системе память, зарезервированную для хранения переменных класса. В примере с классом TMyBox первоначально будет разрушен экземпляр класса TList, ссылка на который содержится в переменной FData. После этого будет вызван деструктор класса TlistBox, в котором разрушается экземпляр класса TStrings. И наконец, будет вызван деструктор класса TObject, где будет освобождена память, зарезервированная для классовых переменных TMyBox.

Вместо прямого вызова деструктора рекомендуется вызывать метод Free, позволяющий проверить, была ли выделена память для разрушаемого экземпляра класса, и если да, то вызывать его деструктор. Использование этого метода важно еще и потому, что деструктор должен быть описан таким образом, чтобы он мог корректно разрушить частично созданный экземпляр класса. Частично созданный экземпляр класса получается в том случае, если в его конструкторе произошло исключение. При этом немедленно вызывается деструктор данного класса, и после его отработки nil-указатель возвращается на создаваемый экземпляр класса. Если, например, в конструкторе резервировалась память под какую-либо переменную (FPBuf):

constructor TMyBox.Create(AOwner:TComponent);
begin
inherited Create(AOwner);
FData:=TList.Create;
GetMem(FPBuf,65500);
end;
destructor TMyBox.Destroy;
begin
FData.Free;
FreeMem(FPBuf);
inherited Destroy;
end;

то исключение может произойти в конструкторе в момент вызова inherited Create или в момент вызова TList.Create — из-за нехватки системных ресурсов. Сразу же будет вызван деструктор, и в момент выполнения оператора FreeMem произойдет генерация еще одного исключения. При этом метод inherited Destroy не будет вызван, а частично созданный экземпляр TMyBox не будет разрушен. Корректная реализация деструктора выглядит так:

if FPBuf<>nil then FreeMem(FPBuf);

При этом в обязательном порядке необходимо проверить, была ли выделена освобождаемая память ранее. Такие проверки необходимо делать со всеми ресурсами, подлежащими освобождению в деструкторе. В противном случае освобождать ресурс лучше в защищенном блоке try…except…end без вызова метода raise в секции except…end. Распространение исключения из деструктора недопустимо (пользователя не должно волновать, что программист не смог корректно высвободить ресурсы!).

Следует отметить, что в случае использования в классе ссылки на какой-либо объект, разрушать его в деструкторе иногда не требуется:

TTest=class(TObject)
private
FData:TList;
public
constructor Create(AData:TList);
end;
implementation
constructor TTest.Create(AData:TList);
begin
inherited Create;
FData:=AData;
end;

Если сам объект AData будет разрушен в той процедуре, где он создан, то переписывать деструктор класса TTest для разрушения объекта FData не требуется. Повторный вызов деструктора приводит к исключению. При этом применение метода Free не спасает, он лишь проверяет, что ссылка на экземпляр класса не указывает на nil.

В отличие от конструктора, для которого может быть определено несколько методов, деструктор бывает только один. Невозможно представить себе ситуацию, когда в классе может понадобиться дополнительный деструктор. Тем не менее компилятор Delphi позволяет это сделать – а зря… Классы с двумя деструкторами – довольно частое явление на распространяемых компонентах для Delphi третьих фирм. Причиной тому программист, забывший директиву override. Это часто приводит к тому, что ресурсы, освобождением которых занимается деструктор, не освобождаются. Во-первых, метод Free обращается к первому виртуальному методу класса – Destroy. При этом будет честно вызван деструктор класса-предка, но ресурсы, освобождение которых программист старательно описывал в деструкторе с забытой директивой override, освобождены не будут. Во-вторых, при разрушении формы содержащиеся на ней компоненты также разрушаются через вызов первого метода в виртуальной таблице, что ведет к аналогичному результату.

В заключение следует рассмотреть на первый взгляд странный вопрос: а всегда ли следует вызывать деструктор (непосредственно или через метод Free) из кода приложения? Правомерность постановки такого вопроса обусловлена тем, что программист нигде не пишет кодов вызова деструкторов компонентов, помещенных на форму на этапе разработки. Ответ заключается в структуре и реализации деструктора класса TComponent. Любой компонент в конструкторе запоминает ссылку на своего хозяина (AOwner) и заносит себя в список компонентов, которыми владеет хозяин. При вызове деструктора компонента он в первую очередь вызывает деструкторы своих «вассалов», и только после этого вызывается собственный деструктор. Таким образом, нет необходимости вызывать деструктор класса TComponent или его потомка – он будет автоматически разрушен при вызове деструктора его хозяина:

TMyBox=class(TListBox)
private
FData:TComponent;
public
constructor Create(AOwner:TComponent);
override;
end;
constructor TMyBox.Create(AOwner:TComponent);
begin
inherited Create(AOwner);
FData:=TComponent.Create(Self);
end;

В данном случае деструктор для разрушения объекта FData не нуждается в переписывании, поскольку он будет разрушен автоматически при разрушении объекта TMyBox. Деструктор для TComponent (или его потомка) следует вызывать только в случае, если его владелец – nil.

Для всех классов–потомков TComponent не следует в явном виде вызывать деструктор. Для всех остальных классов необходимо в коде приложения вызывать деструктор.

В Dephi5 появились два новых виртуальных метода – AfterConstruction и BeforeDestruction, — которые вызываются сразу же после конструктора или перед вызовом деструктора соответственно. Можно поспорить насчет необходимости введения метода BeforeDestruction: любой класс имеет виртуальный деструктор, который можно переписать. Появление метода AfterConstruction следует приветствовать, поскольку виртуальный конструктор появляется только на уровне TComponent в иерархии классов VCL. Появление виртуального конструктора существенно облегчило написание приложений для распределенных вычислений. Например, TComObject – базовый класс для реализации интерфейсов в COM-серверах — является потомком TObject и не содержит виртуального конструктора. Экземпляры этого класса создаются в ответ на запрос клиентов, а не командами из кода приложений, что затрудняет выполнение инициализации переменных при создании экземпляра класса. Введение виртуального метода AfterConstruction сделало инициализацию данных в этих классах рутинной процедурой.

Дополнительную информацию Вы можете получить в компании Interface Ltd.

What is the meaning of the reintroduce and overr > Ask Question

What is the difference between the override and reintroduce directives? And when should I not use the inherited keyword in overridden methods?

4 Answers 4

The reference to Jim’s answer, which was excellent BTW, only described the when and where to use the directives. Another part of the answer is why are they needed anyway? Many languages get along just fine without them, right? When designing aspects of the Delphi Object Pascal Language, OOP (Object Oriented Programming) had been in the mainstream for several years. During this time it was observed that using many of the languages that had adopted those concepts (Turbo Pascal, C++, etc..) to develop application frameworks suffered from what I called the «version 2» problem.

Suppose you developed an awesome framework using language X and released it as version 1. Your users raved at all it could do and it became heavily used. Flush with success, you decide to release version 2 with even more awesomeness. You specifically made sure it was fully backward compatible. Suddenly, you users started reporting strange behaviors. Their own virtual methods were being called at strange times. Many reported that their old code would not compile with the new version. Strange. All the same objects, methods, and functionality still remained. All you did was add a few virtual methods to some base classes, some new object types, and some new optional functionality. What happened?

The override and reintroduce directives serve to eliminate this problem by requiring that in order to actually override a virtual method you must use the override directive in place of the virtual directive. If you happen to introduce your own virtual method that has the same name as one of your ancestors’ virtual methods, the compiler now warns you, but will still do the right thing. In this case, using reintroduce, not only suppresses that warning, it also serves to document in the source you intended to do that.

Without the override and reintroduce directives, you would not be able to continually evolve your framework without fear of breaking all your users. And if your users had to make massive modifications every time a new version is released, then they would be loathe to adopt the new version. Finally, using «override» also allows the framework designer to change the type of virtual in the ancestors without breaking user code. For instance, in Delphi many methods are marked «dynamic» which is a table-based runtime method lookup form of polymorphism. It’s doesn’t perform quite as fast as a regular virtual so it is usually used for methods that are rarely overridden and/or are responses to user actions where the extra overhead is never noticed. Suppose in V1 of the framework a method was marked «dynamic» but in practice it ended up being overridden and called more than you intended. In V2, you could change it to «virtual» without fear of user’s code being broken.

Delphi’s Object Pascal language isn’t the only language to recognize this problem. C# requires the use of an «override» directive for the exact same reason. The C++ standards committee is finally recognizing the problem and are modifying the language to support it. sort of. In C++, if a method’s name and parameter list matches an ancestor’s virtual, then it is an override (even if you don’t say «virtual» on the descendant!). For the upcoming new C++ standard, if you specify «virtual» and the signatures don’t match then it is a new virtual method introduced on current class. If there is a signature match with the ancestor and the writer didn’t intend to override, then the «new» keyword is used to tell the compiler that this is a new virtual for this class.

Virtual — Директива Delphi

Кто-нибудь знает, в чем разница между перекрытием (OVERRIDING) виртуального метода и заменой (REPLACING) его? Я немного запутался.

Допустим у вас есть класс:

и его наследник:

К примеру, TMyObject имеет метод Wiggle:

а TOverrideObject перекрывает Wiggle:

и, естественно, вы реализовали оба метода.

Теперь вы создаете TList, содержащий целую кучу MyObjects и OverrideObjects в свойстве TList.Items[n]. Свойство Items является указателем, поэтому для вызова метода Wiggle вам достаточно вызвать необходимый элемент списка. Например так:

но возможности полиморфизма и директива override позволяют вам сделать так:

Ваше приложение посмотрит на экземпляр специфического объекта, ссылка на который содержится в Items[1] и скажет: «Да, это — TMyObject, но, точнее говоря, это TOverrideObject; но поскольку метод Wiggle является виртуальным методом и TOverrideObject переопределил метод Wiggle, я собираюсь выполнить метод TOverrideObject.Wiggle, а не метод TMyObject.Wiggle.»

Теперь представьте себе, что при декларации метода вы пропустили директиву override, попробуйте это выполнить теперь:

Приложение и в этом случае должно «видеть» данный метод, даже если Items[1] — TOverrideObject; но у него отсутствует перекрытая версия метода Wiggle, поэтому приложение выполнит TMyObject.Wiggle, а не TOverrideObject.Wiggle (поведение, которое вы можете как хотеть, так и избегать).

Так, перекрытый метод функционально может отличаться от декларированного метода, содержащего директиву virtual (или dynamic) в базовом классе, и объявленный с директивой override в классе-наследнике. Для замены метода необходимо объявить его в классе-наследнике без директивы override. Перекрытые методы могут выполняться даже тогда, когда специфический экземпляр класса-предка является точной копией базового класса. «Замененные» методы могут выполняться только тогда, когда специфический экземпляр является «слепком» только этого класса.

Разработка компонентов в среде Delphi

Все компоненты Delphi являются частью иерархии, которая называется Visual Component Library (VCL). Общим предком всех компонентов является класс TComponent (рис. 9.1.1), в котором собран минимальный набор общих для всех компонентов Delphi свойств.

Свойство ComponentState содержит набор значений, указывающих на текущее состояние компонента. Приведем некоторые значения свойства:

csDesigning компонент находится в режиме проектирования
sDestroyingKOMnoHeHT сейчас будет разрушен;
csLoading компонент загружается из файла формы;
csReading компонент считывает значения из файла формы;
csWriting компонент записывает значения своих свойств в поток;
csUpdating компонент вносит изменения, чтобы отразить изменения в родительской форме.

Класс TComponent вводит концепцию принадлежности. Каждый компонент имеет свойство Owner (владелец), ссылающееся на другой компонент как на своего владельца. В свою очередь, компоненту могут принадлежать другие компоненты, ссылки на которые хранятся в свойстве Components. Конструктор ком­понента принимает один параметр, который используется для задания владельца компонента. Если передаваемый владелец су­ществует, то новый компонент добавляется к списку Components владельца. Свойство Components обеспечивает автоматическое разрушение компонентов, принадлежащих владельцу. Свойст­во ComponentCount показывает количество принадлежащих компонентов, a Componentlndex — номер компонента в массиве Components.

В классе TComponent определено большое количество мето­дов. Наибольший интерес представляет метод Notification. Он вызывается всегда, когда компонент вставляется или удаляется из списка Components владельца. Владелец посылает уведомле­ние каждому члену списка Components. Этот метод переопределя­ется в порождаемых классах для того, чтобы обеспечить действи­тельность ссылок компонента на другие компоненты. Например, при удалении компонента Tablel с формы свойство DataSet компонента DataSourcel, равное Tablel, устанавливается в Nil.

Процесс разработки компонента включает пять этапов:

создание модуля компонента;

добавление в новый компонент свойств, методов и событий;

регистрацию компонента в среде Delphi;

На рис. 9.1.1 изображены базовые классы, формирующие структуру VCL. В самом верху расположен TObject, который является предком для всех классов в Object Pascal. От него про­исходит TPersistent, обеспечивающий методы, необходимые для создания потоковых объектов. Потоковый объект — объект, ко­торый может запоминаться в потоке. Поток представляет собой объект, способный хранить двоичные данные (файлы). Поскольку Delphi реализует файлы форм, используя потоки, то TComponent порождается от TPersistent, предоставляя всем компонентам способность сохраняться в файле формы.

Класс TComponent представляет собой вершину иерархии компонентов и является первым из четырех базовых классов, используемых для создания новых компонентов. Прямые по­томки TComponent — невизуальные компоненты.

Вершину иерархии визуальных компонентов представляет класс TControl.

Класс TControl вводит понятие родительских элементов управ­ления (parent control). Свойство Parent является окном, кото­рое содержит элемент управления. Например, если компонент Panel 1 содержит Button 1, то свойство Parent компонента Button 1 равно Panel 1.

Свойство ControlStyle определяет различные стили, приме­нимые только к визуальным компонентам, например:

csAcceptControls элемент управления становится родителем любых элементов управления, помещенных на него во время проектирования. Применим только к оконным элементам управления;
csCaptureMouse элемент управления перехватываетсобытия мыши;
сsFrames элемент управления имеет рамку;
csSetCaption свойства Caption и Text элемента управления (если не заданы явно) устанавливаются так, чтобы совпадать со свойством Name;
csOpaque элемент управления скрывает все элементы позади себя.

В классе TControl определено большинство свойств, использу­емых визуальными компонентами: свойства позиционирования (Align, Left, Top, Height, Width), свойства клиентской области (ClientHeight, ClientWidth), свойства внешнего вида (Color, Enabled, Font, ShowHint, Visible), строковые свойства (Caption, Name, Text, Hint), свойства мыши (Cursor, DragCursor, DragKind, DragMode).

Кроме того, класс TControl реализует методы диспетчеризации событий.

Все визуальные компоненты подразделяют на графические элементы управления и оконные элементы управления. Каж­дый тип представляет свою иерархию классов, происходящую соответственно от TGraphicControl и TWinControl. Главная раз­ница между этими типами компонент состоит в том, что графи­ческие компоненты не поддерживают идентификатор окна, и, соответственно, не могут принять фокус ввода.

Оконные компоненты далее разбиваются на две категории. Прямые потомки TWinControl являются оболочками вокруг су­ществующих элементов управления, реализованных в Windows (например, TEdit, TButton, и др.) и, следовательно, знают, как себя рисовать.

Для компонентов, которые требуют идентификатора окна, но не инкапсулируют базовых элементов Windows, которые бы обеспечивали возможность перерисовывать себя, имеется класс TCustomControl.

9.1.2. Класс TGraphicControl

Класс TGraphicControl является базовым для компонентов, которые не нуждаются в получении фокуса ввода и не служат в качестве родительских для других элементов управления (эти функции требуют наличия идентификатора окна).

По умолчанию объекты TGraphicControl не имеют собствен­ного визуального отображения, но для наследников обеспечи­ваются виртуальный метод Paint (вызывается всегда, когда элемент управления должен быть нарисован) и свойство Canvas (используется как «поверхность» для рисования).

Класс TWinControl используется как базовый для создания компонентов, инкапсулирующих соответствующие оконные эле­менты управления Windows, которые сами себя рисуют.

Класс TWinControl обеспечивает свойство Handle, являюще­еся ссылкой на идентификатор окна базового элемента управ­ления. Кроме этого свойства класс реализует свойства, методы и события, поддерживающие клавиатурные события и измене­ния фокуса:

свойства фокуса TabStop, TabOrder;
свойства внешнего вида Ctl3D, Showing;
методы фокуса CanFocus, Focused;
методы выравнивания AlignControl, EnableAlign, Re Align;
оконные методы CreateWnd, CreateParam, RecreateWnd, CreateWindowHandle, DestroyWnd;
события фокуса OnEnter, OnExit;
события клавиатуры OnKeyDown, OnKeyPress, OnKeyUp.

Создание любого потомка этого класса начинается с вызова ме­тода CreateWnd, который вначале вызывает CreateParams для инициализации записи параметров создания окна, а затем вызыва­ет CreateWindowHandle для создания реального идентификатора окна, использующего запись параметров. Затем CreateWnd настра­ивает размеры окна и устанавливает шрифт элемента управления.

9.1.4. Класс TCustomControl

Класс TCustomControl представляет собой комбинацию клас­сов TWinControl и TGraphicControl. Являясь прямым потомком класса TWinControl, TCustomControl наследует способность управления идентификатором окна и всеми сопутствующими возможностями. Кроме этого, как и класс TGraphicControl, класс TCustomControl обеспечивает потомков виртуальным ме­тодом Paint, ассоциированным со свойством Canvas.

Таким образом, в зависимости от того, какой компонент бу­дет исходным (базовым) для создания нового класса, можно выделить 4 случая:

создание Windows-элемента управления (TWinControl);

создание графического элемента управления (TGraphic-Control);

создание нового элемента управления (TCustomControl); О создание невизуального компонента (TComponent).

9.2. Создание модуля компонента и тестового приложения

Определившись с выбором компонента, можно приступить к созданию модуля компонента. Для этого необходимо выпол­нить следующие шаги.

Выполните команду File/ New. / Component или Component/ New Component.

В диалоговом окне New Component (рис. 9.2.1.) установите основные параметры создания компонента: Ancestor type (имя класса-предка), Class Name (имя класса компонента), Palette Page (вкладка палитры, на которой должен отображаться ком­понент) и Unit file name (имя модуля компонента).

После щелчка на кнопке ОК будет сгенерирован каркас но­вого класса.

По ходу процесса построения компонента необходимо тести­ровать его, не устанавливая в палитру компонентов. Тестовое приложение должно содержать код, который динамически по­мещает новый компонент на форму, изменяет его свойства и вызывает методы.

Илон Маск рекомендует:  Kylix обзор kylix (delphi для linux)

Упражнение 9.2.1. Разработайте новый компонент, который объединяет компоненты TEdit и TLabel. Компонент Label рас­полагается выше поля редактирования (TEdit). При перемеще­нии поля редактирования TLabel следует за ним. При удалении поля редактирования TLabel также удаляется.

В качестве предка класса нового компонента используем TEdit.

Выполните команду Component/ New component. Установите следующие значения параметров окна: Ancestor type TEdit

Class Name TLabelEdit

Palette Page Test

Unit file name . \LabelEdit\LabelEdit.pas

Щелкните на кнопке ОК, автоматически будет сгенерирован следующий код:

Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type

В модуле описан каркас нового класса и написана процедура регистрации компонента (Register), которая помещает его на страницу Test. Сохраните файл модуля компонента.

Разработка тестового приложения

Создайте новый проект. Сохраните его файлы в папке . \LabelEdit: файл модуля — под именем Main.pas, файл про­екта — Test Application, dpr.

Добавьте имя модуля разрабатываемого компонента в раздел Uses формы тестового приложения:

В общедоступный раздел класса TForml добавьте поле

В обработчике события OnCreate формы динамически со­здайте новый компонент:

procedure TForml.FormCreate(Sender: TObject);

Сохраните файлы проекта.

Эксперимент. Убедитесь, что при запуске в левом верхнем углу формы появляется окно редактирования. ♦

9.3. Добавление свойств, методов и событий

Свойства, как и поля класса, являются атрибутом объекта. Но если поля являются простым хранилищем некоего значения, которое может быть прочитано и изменено, то со свойст­вом связаны некоторые действия, осуществляемые при чтении и изменении его содержимого.

Добавление свойства происходит в три этапа.

1. Создание внутреннего поля класса для хранения значения свойства.

2. Описание и разработка методов доступа к значению свойства.

3. Описание свойства.

В классе TControl свойства Caption/Text, Parent и Hint опре­деляются так:

TControl = class (TComponent)

function IsCaptionStored: Boolean;

function IsHintStored: Boolean;

procedure SetText(const Value: TCaption);

property Caption: TCaption read GetText write SetText stored IsCaptionStored;

property Text: TCaption read GetText write SetText;

property Parent: TWinControl read FParent write SetParent;

property Hint: string read FHint write FHint stored IsHintStored;

Объявление свойства имеет следующий синтаксис: property : тип определители;

При объявлении свойства используется зарезервированное слово property, после которого указываются четыре ключевых фрагмента информации. Первый — имя свойства, этот иденти­фикатор используется для ссылок на значение свойства. Таким образом, свойства получают внешний вид полей данных.

Каждое объявление свойства должно определять тип свойства, для этого используется символ двоеточие после имени свойства.

Для указания метода, который будет использоваться для осуществления выборки значения свойства, используется ди­ректива read. Метод должен быть функцией, чей возвращае­мый тип является тем же самым, что и тип свойства.

Однако вместо метода доступа для чтения можно указать внутреннее поле хранения данных, как, например, при описа­нии свойств Hint и Parent. Подобная форма записи приводит к тому, что значение свойства извлекается прямо из внутреннего поля данных.

За спецификацией метода чтения следует определитель ме­тода записи, директива write определяет, какой метод будет ис­пользоваться для присвоения свойству значения. Метод должен быть процедурой, имеющей единственный параметр, тип кото­рого должен совпадать с типом свойства.

При обращении к значению свойства происходит перена­правление на соответствующий метод. Например, оператор s : =Editl. Text; автоматически будет преобразован в оператор s : =Editl. GetText; а оператор Editl. Text: =’ Test’ — в опе­ратор Editl.Text(‘Test’).

Описание свойства должно содержать определитель read или write или сразу оба. Если описание свойства включает в себя только определитель read, то оно является свойством только для чтения. В свою очередь, свойство, чье описание включает в себя только определитель write, является свойством только для записи. При присвоении свойству, определенному с директивой только для чтения, какого-либо значения или при использова­нии в выражении свойства с директивой только для записи все­гда возникает ошибка.

В отличие от внутренних полей хранения данных свойства не могут быть переданы в процедуру (или функцию) в качестве параметра переменной (параметр var), это объясняется тем, что свойство не существует в памяти.

Когда программист использует Инспектор объектов для измене­ния свойств формы или свойств компонентов, то результирующие изменения заносятся в файл формы. Файлы форм представляют собой файлы ресурсов Windows, и когда приложение запускается, то описание формы подгружается из этого файла. Для определения того, что должно сохраняться в файле формы, служат специфика­торы памяти — необязательные директивы stored, default и node-fault. Эти директивы влияют на информацию о типе во время вы­полнения, генерируемую для свойств published.

Директива stored управляет тем, будет или нет свойство дейст­вительно запоминаться в файле формы. За директивой stored дол­жны следовать либо константы True или False, либо имя поля, имеющего тип Boolean, либо имя метода, у которого нет парамет­ров, и возвращающего значение типа Boolean. Например,

property Hint: string read FHint write FHint stored IsHintStored;

Если свойство не содержит директиву stored, то оно рассмат­ривается как содержащее ее с параметром True.

Директивы default и nodefault управляют значениями свой­ства по умолчанию. За директивой default должна следовать константа того же типа, что и свойство, например:

property Tag: Longint read FTag write FTag default 0 ;

Чтобы перекрыть наследуемое значение default без указания нового значения, используется директива nodefault. Директи­вы default и nodefault работают только с порядковыми типами и множествами, нижняя и верхняя границы которых лежат в промежутке от 0 до 31. Если такое свойство описано без дирек­тив default и nodefault, то оно рассматривается как с директи­вой nodefault. Для вещественных типов, указателей и строк значение после директивы default может быть только О, NIL и

(пустая строка) соответственно.

Когда Delphi сохраняет компонент, то просматриваются спе­цификаторы памяти published свойств компонента. Если значе­ние текущего свойства отличается от default значения (или ди­ректива default отсутствует) и параметр stored равен True, то значение свойства сохраняется, иначе свойство не сохраняется.

Спецификаторы памяти не поддерживаются свойствами-мас­сивами, а директива default при описании свойства-массива имеет другое назначение.

Простые свойства — это числовые, строковые и символьные свойства. Они могут непосредственно редактироваться в Инс­пекторе объектов и не требуют специальных методов доступа.

Рассмотрим создание простого свойства Color, описанного в классе TContol (модуль controls.pas):

TControl = class (TComponent)

function IsColorStored: Boolean;

procedure SetColor(Value: TColor);

property Color: TColor read FColor write SetColor stored IsColorStored default clWindow;

function TControl.IsColorStored: Boolean;

Result := not ParentColor;

procedure TControl.SetColor (Value: TColor);

if FColor <> Value then

Perform(CM_COLORCHANGED, 0, 0) ;

9.3.2. Свойства перечислимого типа

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

TShapeType = (stRectangle, stSquare, stRoundRect, stRoundSquare, stEllipse, stCircle);

procedure SetShape(Value: TShapeType);

property Shape: TShapeType read FShape write SetShape

if FShape <> Value then

9.3.3. Свойства типа множества

Свойство типа множества при редактировании в окне Инспек­тора объектов выглядит так же, как множество, определенное синтаксисом языка Pascal. Простейший способ его отредактиро­вать — развернуть свойство в Инспекторе объектов, в результате каждый его элемент станет отдельным логическим значением.

При создании свойства типа множества нужно создать соот­ветствующий тип, описать методы доступа, после чего описать само свойство. В модуле Controls.pas свойсво Align описано сле­дующим образом:

TAlign = (alNone, alTop, alBottom, alLeft, alRight, alClient);

TAlignSet = set of TAlign; TControl = class(TComponent)

procedure SetAlign(Value: TAlign);

property Align: TAlign read FAlign write SetAlign default alNone;

procedure TControl.SetAlign(Value: TAlign);

var OldAlign: TAlign;

if FAlign <> Value then

if not (csLoading in ComponentState) and

(not (csDesigning in ComponentState) or (Parent <> NIL))

if ((OldAlign in [alTop, alBottom])=(Value in [alRight, alLeft])) and not (OldAlign in [alNone, alClient]) and not (Value in [alNone, alClient]) then SetBounds(Left, Top, Height, Width)

в соответствии со значением свойства Align >

Свойства могут являться объектами или другими компонен­тами. Например, у компонента Shape есть свойства-объекты Brush и Реп. Когда свойство является объектом, то оно может быть развернуто в окне инспектора так, чтобы его собственные свойства также могли быть модифицированы. Свойства-объек­ты должны быть потомками класса TPersistent, чтобы их свой­ства, объявленные в разделе published, могли быть записаны в поток данных и отображены в инспекторе объектов.

Для определения объектного свойства компонента необходимо сначала определить объект, который будет использоваться в каче­стве типа свойства. В модуле graphics.pas описан класс TBrush:

procedure GetData(var BrushData: TBrushData);

procedure SetData(const BrushData: TBrushData);

function GetBitmap: TBitmap;

procedure SetBitmap(Value: TBitmap);

function GetColor: TColor;

procedure SetColor(Value: TColor);

function GetHandle: HBrush.;

procedure SetHandle(Value: HBrush);

function GetStyle: TBrushStyle;

procedure SetStyle(Value: TBrushStyle);

constructor Create; destructor Destroy; override;

procedure Assign(Source: TPersistent); override;

property Bitmap: TBitmap read GetBitmap write SetBitmap;

property Handle: HBrush read GetHandle write SetHandle;

property Color: TColor read GetColor write SetColor

property Style: TBrushStyle read GetStyle write SetStyle

Метод Assign предназначен для копирования значения свойств экземпляра TBrush:

Virtual — Директива Delphi

Ребят, может кто поделиться информацией по поводу этих директив? Когда они ставятся, за чем? А то сам ничего толкового не могу найти, кроме дельфисорсес сайта, на котором ничего для меня не понятно.

Директива Abstract определяет метод класса как реализуемый только в классах-наследниках. Он абстрактен в смысле того, что является «заглушкой» — он не имеет реализации в текущем классе, но должен быть реализован классах-наследниках.

Это используется в том случае, когда базовый класс рассматривается как класс-«скелет». Такие классы никогда не используются напрямую — только основанные на них классы используются для создания объектов этих классов.

Например, класс TAnimal может иметь абстрактный метод для задания движения животного. Только когда создается, скажем, класс TCat, основанный на TAnimal, вы можете реализовать этот метод. В этом экземпляре, кот передвигается с помощью ходьбы.

В чем смысл реинтродукции и переопределения директив в Delphi?

В чем разница между директивами override и reintroduce ? И когда я не должен использовать ключевое слово inherited в переопределенных методах?

Ссылка на ответ Джима, который был отличным BTW, описывает только когда и где использовать директивы. Другая часть ответа — зачем они нужны? Многие языки прекрасно ладят без них, верно? При разработке аспектов языка Delphi Object Pascal, ООП (объектно-ориентированное программирование) уже несколько лет находится в русле. За это время было замечено, что использование многих языков, принявших эти концепции (Turbo Pascal, С++ и т.д.) Для разработки рамок приложений, пострадавших от того, что я назвал проблемой «версии 2».

Предположим, что вы разработали потрясающую структуру с использованием языка X и выпустили ее в качестве версии 1. Ваши пользователи обольстили ее вообще, и она стала широко использоваться. Попытайтесь с успехом, вы решите выпустить версию 2 с еще большей простотой. Вы специально удостоверились, что он полностью совместим с обратной связью. Внезапно вы начали рассказывать о странном поведении. Их собственные виртуальные методы вызывались в странные времена. Многие сообщили, что их старый код не будет компилироваться с новой версией. Странно. Все те же объекты, методы и функциональность все еще оставались. Все, что вы сделали, это добавить несколько виртуальных методов к некоторым базовым классам, некоторым новым типам объектов и некоторым новым дополнительным функциям. Что случилось?

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

Без переопределения и повторного введения директив вы не сможете постоянно развивать свою структуру, не опасаясь сломать всех своих пользователей. И если ваши пользователи должны были вносить огромные изменения каждый раз, когда выпускается новая версия, тогда им будет не хватать новой версии. Наконец, использование «переопределения» также позволяет дизайнеру фреймворка изменять тип виртуального в предках без нарушения кода пользователя. Например, в Delphi многие методы отмечены как «динамические», которые представляют собой метод поиска методов поиска на основе таблицы полиморфизма. Он работает не так быстро, как обычный виртуальный, поэтому он обычно используется для методов, которые редко переопределяются и/или являются ответами на действия пользователя, когда лишние служебные данные никогда не замечаются. Предположим, что в V1 рамки метод был отмечен как «динамический», но на практике он оказался переопределенным и назывался больше, чем вы предполагали. В V2 вы можете изменить его на «виртуальный» , не опасаясь сломаться код пользователя.

Язык Delphi Object Pascal не является единственным языком для распознавания этой проблемы. С# требует использования директивы «переопределить» по той же причине. Комитет по стандартам С++, наконец, распознает проблему и модифицирует язык для ее поддержки. своего рода. В С++, если имя метода и список параметров соответствуют виртуальному предку, то это переопределение (даже если вы не говорите «виртуальный» на потомке!). Для нового нового стандарта С++, если вы укажете «виртуальный» , а подписи не совпадают, это новый виртуальный метод, введенный в текущий класс. Если есть соответствие подписи с предком, и автор не намеревался переопределить, тогда ключевое слово «новое» используется, чтобы сообщить компилятору, что это новый виртуальный для этого класса.

Методы в Delphi

Из предыдущего материала читатели узнали, что функционирование объектов обеспечивается различными типами методов, которые различаются особенностями реализации механизма наследования. Теперь настало время рассмотреть эти методы более подробно.

Абстрактными называются методы, которые определены в классе, но не содержат никаких действий, никогда не вызываются и обязательно должны быть переопределены в потомках класса. Абстрактными могут быть только виртуальные и динамические методы. В Object Pascal такие методы объявляются с помощью одноименной директивы. Она указывается при описании метода:

procedure NeverCallMe; virtual; abstract;

При этом никакого кода для этого метода писать не нужно. Вызов метода NeverCallMe приведет к созданию исключительной ситуации EAbstractError (исключительным ситуациям посвящена гл. 4).

Пример с классом TField из разд. «Полиморфизм» этой главы поясняет, для чего нужно использование абстрактных методов. В данном случае класс TField не используется сам по себе; его основное предназначение — быть родоначальником иерархии конкретных классов -«полей» и дать возможность абстрагироваться от частностей. Хотя параметр процедуры showData и описан как TField , но, если передать в нее объект этого класса, произойдет исключительная ситуация вызова абстрактного метода.

Статические методы, а также любые поля в объектах-потомках ведут себя одинаково: вы можете без ограничений перекрывать старые имена и при этом изменять тип методов. Код нового статического метода полностью перекрывает (заменяет собой) код старого метода:

procedure SetData(AValue: Extended);

T2ndObj = class (TlstObj)

procedure SetData(AValue: Integer);

inherited SetData (0.99);

В этом примере разные методы с именем SetData присваивают значения разным полям с именем i. Перекрытое (одноименное) поле предка недоступно в потомке; поэтому, конечно, два одноименных поля с именем i — это нонсенс; так сделано только для примера.

В практике программирования принято присваивать всем идентификаторам в программе (в том числе полям объектов) осмысленные названия. Это существенно облегчит работу с исходным кодом не только другим разработчикам, но и вам.

В отличие от поля, внутри других методов перекрытый метод доступен при указании зарезервированного слова inherited . По умолчанию все методы объектов являются статическими — их адрес определяется еще на стадии компиляции проекта, поэтому они вызываются быстрее всего. Может быть, читателю еще не ясно, для чего упоминается этот факт. Просто запомните его, он понадобится при сравнении статических и виртуальных методов.

Принципиально отличаются от статических виртуальные и динамические методы. Они должны быть объявлены путем добавления соответствующей директивы virtual или dynamic . Обе эти категории существовали и в прежних версиях языка Pascal. С точки зрения наследования методы этих двух видов одинаковы: они могут быть перекрыты в дочернем классе только одноименными методами, имеющими тот же тип.

Если задуматься над рассмотренным выше примером, становится ясно, что у компилятора нет возможности определить класс объекта, фактически переданного в процедуру showData . Нужен механизм, позволяющий определить это прямо во время выполнения. Такой механизм называется поздним связыванием (late binding) .

Естественно, что этот механизм должен быть каким-то образом связан с передаваемым объектом. Для этого используются таблица виртуальных методов (Virtual Method Table, VMT) и таблица динамических методов (Dynamic Method Table, DMT).

Разница между виртуальными и динамическими методами заключается в особенности поиска адреса. Когда компилятор встречает обращение к виртуальному методу, он подставляет вместо прямого вызова по конкретному адресу код, который обращается к VMT и извлекает оттуда нужный адрес.

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

Динамические методы вызываются медленнее, но позволяют более экономно расходовать память. Каждому динамическому методу системой присваивается уникальный индекс. В таблице динамических методов класса хранятся индексы и адреса только тех динамических методов, которые описаны в данном классе. При вызове динамического метода происходит поиск в этой таблице; в случае неудачи просматриваются таблицы DMT всех классов-предков в порядке иерархии и, наконец, класс TObject , где имеется стандартный обработчик вызова динамических методов. Экономия памяти налицо.

Для перекрытия и виртуальных, и динамических методов служит директива override , с помощью которой (и только с ней!) можно переопределять оба этих типа методов. Приведем пример:

What is the meaning of the reintroduce and overr > Ask Question

What is the difference between the override and reintroduce directives? And when should I not use the inherited keyword in overridden methods?

4 Answers 4

The reference to Jim’s answer, which was excellent BTW, only described the when and where to use the directives. Another part of the answer is why are they needed anyway? Many languages get along just fine without them, right? When designing aspects of the Delphi Object Pascal Language, OOP (Object Oriented Programming) had been in the mainstream for several years. During this time it was observed that using many of the languages that had adopted those concepts (Turbo Pascal, C++, etc..) to develop application frameworks suffered from what I called the «version 2» problem.

Suppose you developed an awesome framework using language X and released it as version 1. Your users raved at all it could do and it became heavily used. Flush with success, you decide to release version 2 with even more awesomeness. You specifically made sure it was fully backward compatible. Suddenly, you users started reporting strange behaviors. Their own virtual methods were being called at strange times. Many reported that their old code would not compile with the new version. Strange. All the same objects, methods, and functionality still remained. All you did was add a few virtual methods to some base classes, some new object types, and some new optional functionality. What happened?

The override and reintroduce directives serve to eliminate this problem by requiring that in order to actually override a virtual method you must use the override directive in place of the virtual directive. If you happen to introduce your own virtual method that has the same name as one of your ancestors’ virtual methods, the compiler now warns you, but will still do the right thing. In this case, using reintroduce, not only suppresses that warning, it also serves to document in the source you intended to do that.

Without the override and reintroduce directives, you would not be able to continually evolve your framework without fear of breaking all your users. And if your users had to make massive modifications every time a new version is released, then they would be loathe to adopt the new version. Finally, using «override» also allows the framework designer to change the type of virtual in the ancestors without breaking user code. For instance, in Delphi many methods are marked «dynamic» which is a table-based runtime method lookup form of polymorphism. It’s doesn’t perform quite as fast as a regular virtual so it is usually used for methods that are rarely overridden and/or are responses to user actions where the extra overhead is never noticed. Suppose in V1 of the framework a method was marked «dynamic» but in practice it ended up being overridden and called more than you intended. In V2, you could change it to «virtual» without fear of user’s code being broken.

Delphi’s Object Pascal language isn’t the only language to recognize this problem. C# requires the use of an «override» directive for the exact same reason. The C++ standards committee is finally recognizing the problem and are modifying the language to support it. sort of. In C++, if a method’s name and parameter list matches an ancestor’s virtual, then it is an override (even if you don’t say «virtual» on the descendant!). For the upcoming new C++ standard, if you specify «virtual» and the signatures don’t match then it is a new virtual method introduced on current class. If there is a signature match with the ancestor and the writer didn’t intend to override, then the «new» keyword is used to tell the compiler that this is a new virtual for this class.

Понравилась статья? Поделиться с друзьями:
Кодинг, CSS и SQL