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

Содержание

Основы создания компонент в среде Delphi

Начнем с самого простого — с создания объявления класса. Для этого в меню «Component» выбираем пункт «New Component…». В появившемся окне, нужно задать следующие значения: Ancestor Type (класс родитель создаваемого класса), Class Name (имя создаваемого класса), Palette Page (название закладки палитры компонентов, на которую будет установлен данный компонент; если такой закладки не существует, то она будет создана), Unit File Name (имя модуля, в котором будет размещено описание класса).

Итак, создадим потомок TСustomControl’а — TMyClass, в результате чего получим следующие объявление класса:

Ключевое слово class указывает на то, что мы создаем новый класс TMyClass, порождая его от родителя — TCustomControl.

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

Вот пример использования упреждающего объявления класса:

В соответствии с концепцией объектно-ориентированного программирования (инкапсуляцией) в Delphi существуют четыре директивы видимости, предназначенные для сокрытия полей и методов внутри объекта. Итак, первая из них — private. Это сама строгая из всех директив видимости. Поля и методы, объявленные в этой секции, не будут видны во всех классах-потомках. Вторая, менее строгая — protected. Она предназначена для объявления тех полей и методов, которые должны быть видны в классах-потомках. Следующая директива — public, предназначена для объявления элементов, которые будут видны программе или модулю, если они имеют доступ к модулю, в котором объявлен класс. И, наконец, последняя директива — published. Она предназначена для объявления тех свойств, которые должны быть видны в инспекторе объектов на этапе проектирования (design-time).

Вообще, поля — это обычные переменные, инкапсулированные внутри объекта. Объявляются они точно так же, как и в обычном Паскале (смотри пример ниже, FStr — поле типа String). Также полями могут быть и объекты.

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

Реализация методов производится в том же модуле, где и объявление класса, в секции implementation, следующим образом:

Этот пример также хорошо иллюстрирует сущность инкапсуляции. Класс TMyClass инкапсулирует поле FStr и реализацию методов записи — SetStr и чтения — GetStr этого поля.

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

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

Методы могут быть виртуальными и динамическими. Они различаются способом решения классической проблемы «время — ресурс, ресурс — время» (в данном случае ресурс — это объем оперативной памяти).

Разница между ними заключается в структуре соответствующих таблиц методов VMT — Virtual Metod Table (таблица виртуальных методов) и DMT — Dynamic Metod Table (таблица динамических методов). Диспетчеризация виртуальных методов происходит быстрее, чем динамических, но с другой стороны, таблица виртуальных методов (VMT) занимает больше места в памяти, чем таблица динамических (DMT). Выбор остается за программистом.

Для определения типа метода (виртуальный или динамический) используются две директивы — virtual и dynamic соответственно. Для указания типа метода после его объявления через точку с запятой указывается одна из этих директив. Вот простой пример:

Также стоит сказать и об абстрактных методах — они определяются путем добавления после объявления метода директивы abstract. Такое объявление метода делает ненужным и даже неправомочным реализацию данного метода в текущем классе. Вызов такого метода также будет считаться неправомочным. Такие методы в основном предназначены для перекрытия их в потомках. Пример объявления абстрактного метода:

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

Еще одна разновидность методов — это методы обработки сообщений (message-handling metods). Эти методы специально предназначены для обработки сообщений Windows. На них накладываются некоторые ограничения: они обязательно должны быть процедурами, должны иметь один параметр — переменную; при прикрытии таких методов директива override не применяется; также при перекрытии необходимо вызывать метод предка. Объявляются такие методы с помощью директивы message. Вот пример объявления и реализации метода обработки сообщений:

Константа после директивы message определяет сообщение, которое будет обрабатываться этим методом (в данном случае метод будет вызываться каждый раз, когда происходит перемещение курсора мыши над данным объектом). Тип единственного параметра процедуры задает, в каком формате методу будут переданы данные сообщения (например, в этом случае Msg.XPos и Msg.YPos — текущие координаты курсора мыши в области данного компонента). Описание констант и соответствующих типов помещено в модуле Messages.

Классовые методы (или методы класса)

Существует еще одно важное расширение методов — классовые методы. Это методы, которые можно вызывать, не создавая экземпляра объекта (например: TMyClass.MyClassMetod). В теле классового метода нельзя обращаться ни к одному методу или полю данного класса, но можно вызывать конструкторы и другие классовые методы. Обычно такие методы используются для предоставления информации о классе, версии реализации и т. д. Определяются такие методы путем добавления директивы class перед объявлением метода. Вот пример классового метода и его реализации:

Перерегружаемые (overload) методы

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

В данном примере, при вызове пользователем метода SetX, в зависимости от типа передаваемого параметра будет вызван соответствующий метод (для параметра типа String — первый, для Integer — второй).

Пример вызова первого варианта метода:

Пример вызова второго варианта метода:

Удобства, предоставляемые данной возможностью, очевидны, например, в данном случае пользователю не нужно самому конвертировать Integer в String.

Конструкторы и деструкторы

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

Свойства — это второе по важности (после методов) средство обеспечения взаимодействия объекта с окружающим миром. Через свойства обеспечивается доступ к полям объекта, только свойства могут быть видны в инспекторе объектов (Object Inspector) Delphi.

Объявление свойства, чтение и запись.

Вот один из вариантов объявления свойства:

Свойства объявляются с использованием директивы property, за ней следует имя свойства и тип (как объявление переменной в обычном Паскале), затем директива read и указание «источника» чтения (это либо поле такого же типа, как свойство, либо функция без параметров, возвращающая значение того же типа), далее следует директива write с указанием «приемника» записи (поле с тем же типом, что и свойство, либо процедура с одним параметром, того же типа). В нашем случае чтение осуществляется напрямую из поля, а запись через процедуру. Здесь есть одна хитрость. Соответствующее поле не обязательно должно существовать или иметь тот же тип, что и свойство. Это позволяет снизить избыточность хранимой информации и уменьшить расход памяти. Распространенным примером такого подхода является предоставление информации о размере какого-либо массива, например так:

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

property StrCount: Integer read GetStrCount; — свойство только для чтения (read-only)
property StrCount: Integer write SetStrCount; — свойство только для записи (write-only).

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

Теперь разберемся, как организовать свойство, предоставляющее доступ непосредственно к элементам массива — строкам для предыдущего примера. Для этого обязательно чтение и запись производить через соответствующий метод, добавляя в его описание один параметр — индекс, и задать тип индекса для самого свойства. На практике это выглядит так:

Тип индекса может быть и другим, например строкой.

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

К таким свойствам следует обращаться, как к массивам, например для экземпляра Test класса TMyClass — Test.Str[5].

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

Как видно из примера, если для свойства определен индекс, совершенно необязательно для чтения или записи использовать метод, поддерживающий индексирование. Методом чтения и записи в качестве индекса (параметр Index, если у них таковой имеется) для каждого свойства будет передаваться то значение, которое указано после директивы index. Ну а в теле метода по полученному значению индекса выясняем, к какому из свойств было осуществлено обращение, и предпринимаем соответствующие действия.

Свойства совсем необязательно объявлять в public или published секциях. Очень часто наоборот бывает удобно объявить свойство в секции protected для последующего переобъявления его в классах-потомках. Но не все так хорошо — нельзя понизить видимость, т. е. если свойство объявлено в классе-предке в секции public, то переобъявив его в потомке в секции protected, невидимым его сделать не получится.

Переобъявляются свойства очень просто, возьмем наш пример с массивом строк:

Эта директива применяется для задания значения по умолчанию для свойств. На практике выглядит так:

Тип свойства должен совпадать с типом значения, указываемого после default’а.

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

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

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

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

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

На такое свойство можно ссылаться без указания его имени, например для экземпляра MyClass класса TMyClass — MyClass[10] равносильно MyClass.Str[10].

Эта директива предназначена для жесткого определения — сохранять значение свойства в файле формы или нет. Она добавляется в конец определения свойства, и после нее должно идти либо булево (boolean — FALSE или TRUE) значение, либо функция, возвращающая булево значение, либо поле булевого типа. Если булево значение ИСТИНА (TRUE), то сохранение значения свойства происходит, если ЛОЖЬ (FALSE) — нет. Этот механизм может быть полезен, если существует какая-либо избыточность полей и соответствующих им свойств. Возможно совместное использование директив default и stored. Пример (взят из модуля Controls):

События — это единственный для объекта способ спровоцировать какое-либо внешнее действие в ответ на изменение его состояния.

Событие как свойство

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

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

Само объявление же события будет выглядеть так:

Вызывается сообщение путем обращения к соответствующему полю как к процедуре или функции. Для предыдущего примера это будет выглядеть так:

Перед вызовом события обязательно нужно проверить, существует ли обработчик этого события — это выполняется с помощью функции Assigned. Если этого не сделать, то при отсутствии обработчика получим ошибку времени выполнения, доступа к памяти. Self — внутри каждого объекта — это указатель на самого себя, который мы и передаем в качестве параметра Sender.

Общепринятые правила именования

Напоследок стоит сказать об общепринятых правилах именования классов и их составляющих:

  • из названия должно быть понятно, для чего предназначен данный класс, метод, поле или свойство;
  • название классов принято называть с T, от type;
  • к названию иногда добавляют инициалы разработчика компонента или часть названия фирмы. Для Васи Иванова это может быть — TviComponent;
  • имена полей принято начинать с F, от Field (поле, область);
  • свойства принято называть точно так же, как и поля (если свойству в соответствие ставится поле), опуская F. Поле FStr — свойство Str;
  • имена методов чтения и записи принято образовывать путем добавления перед именем свойства Get и Set, соответственно. Свойство Str — метод чтения GetStr, метод записи SetStr;
  • именование полей и методов чтения и записи для событий подчиняются тем же правилам, что и для свойств;
  • имена событий принято начинать с On: OnClick, OnMouseMove.

Для того чтобы установить созданный класс в палитру компонентов, в меню «Component» выбираем пункт «Install Component…». В появившемся окне нужно задать следующие значения: Unit file name (имя модуля, в котором содержится описание устанавливаемого класса), Search path (пути через «;» по которым Delphi будет искать указанный модуль), Package file name (путь и имя файла пакета, в который будет добавлен устанавливаемый вами модуль с описанием класса).

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

Наверняка, в предыдущих уроках вы задавались вопросом: что же обозначает ключевое слово public в описании класса? Именно об этом мы сейчас и поговорим.
Но сначала мы вернемся к истокам объекто-ориентированного программирования, а вернее к тем парадигмам, которые лежат в основе ООП. Одной из наиболее важных целей ООП является возможность отделить внутреннюю реализацию класса от внешнего использования и реализовывать класс так, чтобы он выполнял только строго задуманную задачу. При этом необходимо отделить реализацию задуманной задачи в классе от возможностей внешнего использования этого класса. Такой подход называется инкапсуляцией. Это такой механизм ООП, который позволяет ограничить доступ к составляющим класса (методам и полям. И необходимо его использовать для того, чтобы, к примеру, ограничить внешний доступ к каким-либо составляющим класса, которые обеспечивают внутреннюю реализацию самого класса. Т.е. те методы и поля, которые использовать извне не рекомендуется, которые служат для внутреннего функционирования, работы класса, будет грамотнее сделать недоступными для обращения из внешнего кода класса. Внешним кодом для класса в данном случае мы называем код, который не является реализацией данного класса. Ранее мы видели подобную конструкцию:
[cc lang=»delphi»]TTeacher = class
public
FullName: string;

constructor Create(NewFullName: string);
destructor Destroy;
end;[/cc]

Ключевое слово public указывает на то, что последующие поля и методы будут доступны во внешнем коде у данного класса. Т.е. все, что следует после public будет доступно отовсюду при обращении к экземпляру данного класса.

В классе можно объявить несколько модификаторов доступа. Например, добавим модификатор доступа private:
[cc lang=»delphi»]TTeacher = class
private
BaseId: integer; // Предположим, что это какой-либо идентификатор в базе данных, где хранятся данные об этом преподавателе.

procedure SaveToBase; // А это внутренний метод, который вероятно предназначен для записи обновленных данных в БД.
public
FullName: string;

constructor Create(NewFullName: string);
destructor Destroy;
end;[/cc]
Как вы видите, я описал одно поле и один метод, которые явно должны быть скрыты от внешнего использования, которые наверняка будут использоваться только внутри реализации самого класса. Именно поэтому мы будем использовать модификатор доступа private.

Private позволяет полностью ограничить доступ к ниже перечисленным полям и методам. При попытке к ним обратиться из внешнего кода они будут невидимы, попытки к ним обратиться будут запрещаться ошибкой компиляции.
[warning]Существует единственное ограничение для private — все поля и методы, находящиеся в этой группе доступа, будут в любом случае видимы внутри текущего юнита, в котором описан и реализован класс. Ограничение НЕ работает в юните, в котором описан и реализован класс, оно распространяется лишь на другие юниты.[/warning]
[tip]Одной из задач ООП является организация командной работы над проектом. Если какое-то поле или какой-то метод был вынесен в приватную группу доступа класса, то значит разработчик этого класса не рекомендует обращаться к этим полям и методам из внешнего кода, он просто ограничивает к ним доступ. В публичную группу доступа рекомендуется выносить только те поля и методы, которые предназначены для обращения к ним извне, для работы с классом из внешнего кода.[/tip]
Иногда возникает потребность в том, чтобы ограничить использование какого-либо метода или поля и во внешнем коде текущего юнита (в котором описан и реализован данный класс) тоже. Для этого существует группа доступа strict private.

Strict private — группа доступа, которая появилась в Delphi относительно недавно. Она позволяет, как уже было сказано, ограничить доступ к определенным полям и методам и в юните, в котором реализован и описан данный класс, в отличие от группы доступа private. Т.е. эта группа доступа действительно запрещает использование во всем внешнем коде, в том числе и в данном юните. Таким образом, ранее приведенный пример описания класса может выглядеть так:
[cc lang=»delphi»]TTeacher = class
strict private
BaseId: integer; // Предположим, что это какой-либо идентификатор в базе данных, где хранятся данные об этом преподавателе.

procedure SaveToBase; // А это внутренний метод, который вероятно предназначен для записи обновленных данных в БД.
public
FullName: string;

constructor Create(NewFullName: string);
destructor Destroy;
end;[/cc]
Итак, в этом уроке мы разобрали три группы доступа: private, strict private и public. И, конечно же, рассмотрели различия между ними. В следующем уроке мы рассмотрим принципы наследования классов — это тоже одна из наиболее важных парадигм. И при ее рассмотрении у нас появится еще одна группа доступа protected, но это уже совсем другая история…

Delphi «частный» пункт (директива) не работает

Я пытаюсь проверить, если мои личные процедуры действительно частные. Но это работает так, как он не должен.

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

Этот код не должен работать. Я полагаю. Но это работает.

Спасибо. (Delphi-7, Win7 x64).

private является для unit .

С более новой версией Delphi, вы можете использовать , strict private чтобы получить ожидаемое поведение.

Частные, Protected и открытые члены

Частный член невидим снаружи устройства или программы , в которой объявлен его класс. Другими словами, частный метод не может быть вызван из другого модуля, и частное поле или свойство не может быть записана или считана из другого модуля. Размещая объявления связанных классов в том же модуле, вы можете дать доступ классов к другу друг частных пользователей без этих членов более широко доступных. Для члена , чтобы быть видимым только внутри своего класса, он должен быть объявлен строгим частными .

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

Общественный элемент виден везде , где можно ссылаться его класс.

Строгие Видимость спецификаторы

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

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

Традиционная Деая частная видимость Спецификатор карты для видимости сборок СЬКА. Delphi , защищенная видимость Спецификатор карты для сборки или семьи видимости в СЬКЕ.

Примечание : Слово строго рассматриваются как директива в контексте описания класса. В объявлении класса вы не можете объявить элемент с именем «строгими», но это приемлемо для использования за пределами объявления класса.

Ваша версия Delphi, Delphi 7 не поддерживает strict описатели.

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

Классы очень удобно собирать в модули. При этом их описание помещается в секцию interface, а код методов — в секцию implementation. Создавая модули классов, нужно придерживаться следующих правил:

  • все классы, предназначенные для использования за пределами модуля, следует определять в секции interface;
  • описание классов, предназначенных для употребления внутри модуля, следует располагать в секции implementation;
  • если модуль B использует модуль A, то в модуле B можно определять классы, порожденные от классов модуля A.

Соберем рассмотренные ранее классы TTextReader, TDelimitedReader и TFixedReader в отдельный модуль ReadersUnit:

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

Разграничение доступа к атрибутам объектов

Программист может разграничить доступ к атрибутам своих объектов для других программистов (и себя самого) с помощью специальных ключевых слов: private, protected, public, published (последнее не используется в модуле ReadersUnit).

  • Private. Все, что объявлено в секции private недоступно за пределами модуля. Секция private позволяет скрыть те поля и методы, которые относятся к так называемым особеностям реализации. Например, в этой секции класса TTextReader объявлены поля FFile, FActive и FItems, а также методы PutItem, SetActive, GetItemCount и GetEndOfFile.
  • Public. Поля, методы и свойства, объявленные в секции public не имеют никаких ограничений на использование, т.е. всегда видны за пределами модуля. Все, что помещается в секцию public, служит для манипуляций с объектами и составляет программный интерфейс класса. Например, в классе TTextReader в эту секцию помещены конструктор Create, метод NextLine, свойства Active, Items, ItemCount.
  • Protected. Поля, методы и свойства, объявленные в секции protected, видны за пределами модуля только потомкам данного класса; остальным частям программы они не видны. Так же как и private, директива protected позволяет скрыть особенности реализации класса, но в отличие от нее разрешает другим программистам порождать новые классы и обращаться к полям, методам и свойствам, которые составляют так называемый интерфейс разработчика. В эту секцию обычно помещаются виртуальные методы. Примером такого метода является ParseLine.
  • Published. Устанавливает правила видимости те же, что и директива public. Особенность состоит в том, что для элементов, помещенных в секцию published, компилятор генерирует информацию о типах этих элементов. Эта информация доступна во время выполнения программы, что позволяет превращать объекты в компоненты визуальной среды разработки. Секцию published разрешено использовать только тогда, когда для самого класса или его предка включена директива компилятора $TYPEINFO.

Перечисленные секции могут чередоваться в объявлении класса в произвольном порядке, однако в пределах секции сначала следует описание полей, а потом методов и свойств. Если в определении класса нет ключевых слов private, protected, public и published, то для обычных классов всем полям, методам и свойствам приписывается атрибут видимости public, а для тех классов, которые порождены от классов библиотеки VCL, — атрибут видимости published.

Внутри модуля никакие ограничения на доступ к атрибутам классов, реализованных в этом же модуле, не действуют. Кстати, это отличается от соглашений, принятых в некоторых других языках программирования, в частности в языке C++.

Указатели на методы объектов

В языке Delphi существуют процедурные типы данных для методов объектов. Внешне объявление процедурного типа для метода отличается от обычного словосочетанием of object, записанным после прототипа процедуры или функции:

Переменная такого типа называется указателем на метод (method pointer). Она занимает в памяти 8 байт и хранит одновременно ссылку на объект и адрес его метода.

Методы объектов, объявленные по приведенному выше шаблону, становятся совместимы по типу со свойством OnReadLine.

Если установить значение свойства OnReadLine:

и переписать метод NextLine,

то объект Form1 через метод HandleLine получит уведомление об очередной считанной строке. Обратите внимание, что вызов метода через указатель происходит лишь в том случае, если указатель не равен nil. Эта проверка выполняется с помощью стандартной функции Assigned, которая возвращает True, если ее аргумент является связанным указателем.

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

Метаклассы

Ссылки на классы

Язык Delphi позволяет рассматривать классы объектов как своего рода объекты, которыми можно манипулировать в программе. Такая возможность рождает новое понятие — класс класса; его принято обозначать термином метакласс.

Для поддержки метаклассов введен специальный тип данных — ссылка на класс (class reference). Он описывается с помощью словосочетания class of, например:

Переменная типа TTextReaderClass объявляется в программе обычным образом:

Значениями переменной ClassRef могут быть класс TTextReader и все порожденные от него классы. Допустимы следующие операторы:

По аналогии с тем, как для всех классов существует общий предок TObject, у ссылок на классы существует базовый тип TClass, определенный, как:

Переменная типа TClass может ссылаться на любой класс.

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

Физический смысл и взаимосвязь таких понятий, как переменная-объект, экземпляр объекта в памяти, переменная-класс и экземпляр класса в памяти поясняет рисунок 4.

Рисунок 4. Переменная-объект, экземпляр объекта в памяти, переменная-класс и экземпляр класса в памяти

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

Метаклассы привели к возникновению нового типа методов — методов класса. Метод класса оперирует не экземпляром объекта, а непосредственно классом. Он объявляется как обычный метод, но перед словом procedure или function записывается зарезервированное слово class, например:

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

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

Метод ClassName объявлен в классе TObject и возвращает имя класса, к которому применяется. Очевидно, что надуманный метод GetClassName просто дублирует эту функциональность для класса TTextReader и всех его наследников.

Методы класса применимы и к классам, и к объектам. В обоих случаях в параметре Self передается ссылка на класс объекта. Пример:

Методы классов могут быть виртуальными. Например, в классе TObject определен виртуальный метод класса NewInstance. Он служит для распределения памяти под объект и автоматически вызывается конструктором. Его можно перекрыть в своем классе, чтобы обеспечить нестандартный способ выделения памяти для экземпляров. Метод NewInstance должен перекрываться вместе с другим методом FreeInstance, который автоматически вызывается из деструктора и служит для освобождения памяти. Добавим, что размер памяти, требуемый для экземпляра, можно узнать вызовом предопределенного метода класса InstanceSize.

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

Особая прелесть ссылок на классы проявляется в сочетании с виртуальными конструкторами. Виртуальный конструктор объявляется с ключевым словом virtual. Вызов виртуального конструктора происходит по фактическому значению ссылки на класс, а не по ее формальному типу. Это позволяет создавать объекты, классы которых неизвестны на этапе компиляции. Механизм виртуальных конструкторов применяется в среде Delphi при восстановлении компонентов формы из файла. Восстановление компонента происходит следующим образом. Из файла считывается имя класса. По этому имени отыскивается ссылка на класс (метакласс). У метакласса вызывается виртуальный конструктор, который создает объект нужного класса.

На этом закончим изучение теории объектно-ориентированного программирования и в качестве практики рассмотрим несколько широко используемых инструментальных классов среды Delphi. Разберитесь с их назначением и работой. Это поможет глубже понять ООП и пригодится на будущее.

Классы общего назначения

Как показывает практика, в большинстве задач приходится использовать однотипные структуры данных: списки, массивы, множества и т.д. От задачи к задаче изменяются только их элементы, а методы работы сохраняются. Например, для любого списка нужны процедуры вставки и удаления элементов. В связи с этим возникает естественное желание решить задачу «в общем виде», т.е. создать универсальные средства для управления основными структурами данных. Эта идея не нова. Она давно пришла в голову разработчикам инструментальных пакетов, которые быстро наплодили множество вспомогательных библиотек. Эти библиотеки содержали классы объектов для работы со списками, коллекциями (динамические массивы с переменным количеством элементов), словарями (коллекции, индексированные строками) и другими «абстрактными» структурами. Для среды Delphi тоже разработаны аналогичные классы объектов. Их большая часть сосредоточена в модуле Classes. Наиболее нужными для вас являются списки строк (TStrings, TStringList) и потоки (TSream, THandleSream, TFileStream, TMemoryStream и TBlobStream). Рассмотрим кратко их назначение и применение.

Классы для представления списка строк

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

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

Свойства класса TStrings описаны ниже.

  • Count: Integer — число элементов в списке.
  • Strings[Index: Integer]: string — обеспечивает доступ к массиву строк по индексу. Первая строка имеет индекс, равный 0. Свойство Strings является основным свойством объекта.
  • Objects[Index: Integer]: TObject — обеспечивает доступ к массиву объектов. Свойства Strings и Objects позволяют использовать объект TStrings как хранилище строк и ассоциированных с ними объектов произвольных классов.
  • Text: string — позволяет интерпретировать список строк, как одну большую строку, в которой элементы разделены символами #13#10 (возврат каретки и перевод строки).

Наследники класса TStrings иногда используются для хранения строк вида Имя=Значение, в частности, строк INI-файлов (см. гл. 6). Для удобной работы с такими строками в классе TStrings дополнительно имеются следующие свойства.

  • Names[Index: Integer]: string — обеспечивает доступ к той части строки, в которой содержится имя.
  • Values[const Name: string]: string — обеспечивает доступ к той части строки, в которой содержится значение. Указывая вместо Name ту часть строки, которая находится слева от знака равенства, вы получаете ту часть, что находится справа.

Управление элементами списка осуществляется с помощью следующих методов:

  • Add(const S: string): Integer — добавляет новую строку S в список и возвращает ее позицию. Новая строка добавляется в конец списка.
  • AddObject(const S: string; AObject: TObject): Integer — добавляет в список строку S и ассоциированный с ней объект AObject. Возвращает индекс пары строка-объект.
  • AddStrings(Strings: TStrings) — добавляет группу строк в существующий список.
  • Append(const S: string) — делает то же, что и Add, но не возвращает значения.
  • Clear — удаляет из списка все элементы.
  • Delete(Index: Integer) — удаляет строку и ассоциированный с ней объект. Метод Delete, также как метод Clear не разрушают объектов, т.е. не вызывают у них деструктор. Об этом вы должны позаботиться сами.
  • Equals(Strings: TStrings): Boolean — Возвращает True, если список строк в точности равен тому, что передан в параметре Strings.
  • Exchange(Index1, Index2: Integer) — меняет два элемента местами.
  • GetText: PChar — возвращает все строки списка в виде одной большой нуль-терминированной строки.
  • IndexOf(const S: string): Integer — возвращает позицию строки S в списке. Если заданная строка в списке отсутствует, функция возвращает значение -1.
  • IndexOfName(const Name: string): Integer — возвращает позицию строки, которая имеет вид Имя=Значение и содержит в себе Имя, равное Name.
  • IndexOfObject(AObject: TObject): Integer — возвращает позицию объекта AObject в массиве Objects. Если заданный объект в списке отсутствует, функция возвращает значение -1.
  • Insert(Index: Integer; const S: string) — вставляет в список строку S в позицию Index.
  • InsertObject(Index: Integer; const S: string; AObject: TObject) — вставляет в список строку S и ассоциированный с ней объект AObject в позицию Index.
  • LoadFromFile(const FileName: string) — загружает строки списка из текстового файла.
  • LoadFromStream(Stream: TStream) — загружает строки списка из потока данных (см. ниже).
  • Move(CurIndex, NewIndex: Integer) — изменяет позицию элемента (пары строка-объект) в списке.
  • SaveToFile(const FileName: string) — сохраняет строки списка в текстовом файле.
  • SaveToStream(Stream: TStream) — сохраняет строки списка в потоке данных.
  • SetText(Text: PChar) — загружает строки списка из одной большой нуль-терминированной строки.

Класс TStringList добавляет к TStrings несколько дополнительных свойств и методов, а также два свойства-события для уведомления об изменениях в списке. Они описаны ниже.

Свойства:

  • Duplicates: TDuplicates — определяет, разрешено ли использовать дублированные строки в списке. Свойство может принимать следующие значения: dupIgnore (дубликаты игнорируются), dupAccept (дубликаты разрешены), dupError (дубликаты запрещены, попытка добавить в список дубликат вызывает ошибку).
  • Sorted: Boolean — если имеет значение True, то строки автоматически сортируются в алфавитном порядке.

Методы:

  • Find(const S: string; var Index: Integer): Boolean — выполняет поиск строки S в списке строк. Если строка найдена, Find помещает ее позицию в переменную, переданную в параметре Index, и возвращает True.
  • Sort — сортирует строки в алфавитном порядке.

События:

  • OnChange: TNotifyEvent — указывает на обработчик события, который выполнится при изменении содержимого списка. Событие OnChange генерируется после того, как были сделаны изменения.
  • OnChanging: TNotifyEvent — указывает на обработчик события, который выполнится при изменении содержимого списка. Событие OnChanging генерируется перед тем, как будут сделаны изменения.

Ниже приводится фрагмент программы, демонстрирующий создание списка строк и манипулирование его элементами:

Классы для представления потока данных

В среде Delphi существует иерархия классов для хранения и последовательного ввода-вывода данных. Классы этой иерархии называются потоками. Потоки лучше всего представлять как файлы. Классы потоков обеспечивают различное физическое представление данных: файл на диске, раздел оперативной памяти, поле в таблице базы данных (таблица 1).

Таблица 1. Классы потоков

Класс

Описание

TStream Абстрактный поток, от которого наследуются все остальные. Свойства и методы класса TStream образуют базовый интерфейс потоковых объектов.
THandleStream Поток, который хранит свои данные в файле. Для чтения-записи файла используется дескриптор (handle), поэтому поток называется дескрипторным. Дескриптор — это номер открытого файла в операционной системе. Его возвращают низкоуровневые функции создания и открытия файла.
TFileStream Поток, который хранит свои данные в файле. Отличается от ThandleStream тем, что сам открывает (создает) файл по имени, переданному в конструктор.
TMemoryStream Поток, который хранит свои данные в оперативной памяти. Моделирует работу с файлом. Используется для хранения промежуточных результатов, когда файловый поток не подходит из-за низкой скорости передачи данных.
TResourceStream Поток, обеспечивающий доступ к ресурсам в Windows-приложении.
TBlobStream Обеспечивает последовательный доступ к большим полям таблиц в базах данных.

Потоки широко применяются в библиотеке VCL и наверняка вам понадобятся. Поэтому ниже кратко перечислены их основные общие свойства и методы.

Общие свойства:

  • Position: Longint — текущая позиция чтения-записи.
  • Size: Longint — текущий размер потока в байтах.

Общие методы:

  • CopyFrom(Source: TStream; Count: Longint): Longint — копирует Count байт из потока Source в свой поток.
  • Read(var Buffer; Count: Longint): Longint — читает Count байт из потока в буфер Buffer, продвигает текущую позицию на Count байт вперед и возвращает число прочитанных байт. Если значение функции меньше значения Count, то в результате чтения был достигнут конец потока.
  • ReadBuffer(var Buffer; Count: Longint) — читает из потока Count байт в буфер Buffer и продвигает текущую позицию на Count байт вперед. Если выполняется попытка чтения за концом потока, то генерируется ошибка.
  • Seek(Offset: Longint; Origin: Word): Longint — продвигает текущую позицию в потоке на Offset байт относительно позиции, заданной параметром Origin. Параметр Origin может иметь одно из следующих значений: 0 — смещение задается относительно начала потока; 1 — смещение задается относительно текущей позиции в потоке; 2 — смещение задается относительно конца потока.
  • Write(const Buffer; Count: Longint): Longint — записывает в поток Count байт из буфера Buffer, продвигает текущую позицию на Count байт вперед и возвращает реально записанное количество байт. Если значение функции отличается от значения Count, то при записи была ошибка.
  • WriteBuffer(const Buffer; Count: Longint) — записывает в поток Count байт из буфера Buffer и продвигает текущую позицию на Count байт вперед. Если по какой-либо причине невозможно записать все байты буфера, то генерируется ошибка.

Ниже приводится фрагмент программы, демонстрирующий создание файлового потока и запись в него строки:

Итоги

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

Разработка компонентов в среде 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 (имя модуля компонента).

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

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

Упражнение 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:

Блог GunSmoker-а

. when altering one’s mind becomes as easy as programming a computer, what does it mean to be human.

6 февраля 2013 г.

«Дружественность» в Delphi

Содержание

Инкапсуляция

Инкапсуляция — один из четырёх важнейших механизмов объектно-ориентированного программирования, заключающийся в объединении данных с методами и скрытии реализации (т.н. «чёрный ящик» — доступность главного, сокрытие второстепенного). Инкапсуляцию применяют для обеспечения согласованности внутреннего состояния объекта. Применение инкапсуляции позволяет предельно локализовать изменения (при необходимости таких изменений), прогнозировать изменения (какие изменения в коде нужно сделать для заданного изменения функциональности) и прогнозировать последствия изменений.

В ООП языках программирования инкапсуляция реализуется через механизмы ограничения доступа к членам класса путём введения ключевых слов типа private или protected . Общая идея состоит в том, что каждый класс выставляет наружу публичный ( public ) интерфейс, который и позволяет управлять объектом. Все открытые методы интерфейса объекта гарантировано переводят объект из одного согласованного и корректного состояния в другое. При этом внутренние «кишки» объекта скрыты от внешнего доступа, так что никакой сторонний код не может перевести объект из согласованного состояния в недопустимое. Однако применение директив вида protected позволяет наследникам класса получать доступ к его внутренним членам.

Необходимость обхода инкапсуляции

Но объекты не существуют автономно, сами по себе. Поэтому на практике часто возникают ситуации, когда необходимо обеспечить более тесное взаимодействие объектов, не связанных иерархией. Это подразумевает, что один объект должен уметь обращаться к другому объекту в обход его публичного интерфейса. Простой пример: объект-коллекция и объект-элемент коллекции. Эти два класса, скорее всего, не будут наследоваться друг от друга, а будут двумя разными ветвями дерева наследования. Тем не менее, при добавлении элемента в коллекцию нужно установить владельца (для элемента), его индекс и другие данные для учёта. Само собой, подобный сервис не должен выставляться наружу, поскольку тогда сторонний код мог бы его менять и, таким образом, «испортить» данные объекта. Так что публичный интерфейс будет содержать методы типа «добавить элемент в коллекцию», но не «теперь этот объект — в той коллекции, а его индекс там — минус сто».

Например: Здесь TCollection — коллекция, а TCollectionItem — элемент коллекции. Оба класса наследуются от общего предка TPersistent , но не друг от друга. И TCollection и TCollectionItem предоставляют открытый интерфейс для управления: метод добавления ( Add ) и получения информации (свойство Collection ), но не позволяют менять их на произвольные значения стороннему коду (закрытые поля FCollection и FItems ).

Илон Маск рекомендует:  Описание форматов звуковых файлов выборок

Разумеется, при добавлении элемента в коллекцию, необходимо внести элемент в список элементов ( FItems ) и назначить ему владельца ( FCollection ), поэтому этим двум классам необходимо иметь доступ к закрытым элементам друг друга в обход общего механизма разграничения доступа.

Понятие класса, дружественного другому классу

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

В Delphi нет специальных языковых конструкций для выделения дружественных классов. Вместо этого здесь используется иной механизм. А именно: любые классы в одном модуле (unit) автоматически считаются дружественными друг к другу и могут обращаться к закрытым элементам друг друга. Иными словами, расположение классов в одном модуле является необходимым и достаточным условием дружественности классов друг другу.

Поэтому в примере выше TCollection и TCollectionItem являются дружественными, поскольку расположены в одном модуле, так что допускается код типа такого: или такого:

Проблемы дружественности в Delphi

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

Сужение дружественности: strict

Понимая, что не всякий класс в модуле должен быть дружественным к другим классам этого же модуля, разработчики Delphi ввели новое ключевое слово strict , которое в комбинации с private и protected позволяют ограничивать доступ к членам класса для других классов в этом же модуле. Так, к примеру, к элементам класса в strict private не может получить доступа ни один код вне этого же класса, даже если этот код находится в том же модуле. Аналогично, доступ к элементам strict protected будут иметь только наследники этого класса, но не другие классы, даже если они будут находится в том же модуле.

Расширение дружественности: хак

Со второй проблемой (дружественные классы в разных модулях) несколько сложнее. В настоящее время Delphi не предоставляет никаких специальных языковых конструкций для разрешения этого конфликта. Вы не можете объявить классы дружественными друг другу, если они находятся в разных модулях. Поэтому программисту придётся использовать обходные пути.

Первый путь — самый простой и прямолинейный. Он заключается в использовании хака. Хотя обычно хак — это плохо, но данный конкретный хак безопасен. Заключается он в объявлении пустого класса-наследника в том же модуле, где нужно сделать дружественный класс. Например, пусть у нас есть класс: И в другом модуле есть класс, который должен обращаться к полю FItem : Чтобы разрешить этот конфликт (подразумевая, что мы не можем поместить TSomeClass и TAnotherClass в один модуль), мы можем сделать поле FItem как protected А для TAnotherClass использовать такую конструкцию: Объявляя класс-наследник ( TDummySomeClass ), мы делаем доступными ему все protected члены (и не важно, в каком модуле они расположены). А то, что этот класс-заглушка объявлен именно в модуле второго класса ( TAnotherClass ), сделает эти два класса дружественными, что и даст доступ к закрытому полю FItem . Обратите внимание, что при этом исходный класс ( TSomeClass ) не становится дружественным к TAnotherClass — вот почему нам необходимо выполнять явное преобразование типов.

Насколько безопасен такой хак? Ведь мы приводим объект одного класса ( TSomeClass ) к другому ( TDummySomeClass ), хотя он им не является. Ответ: достаточно безопасен. TDummySomeClass не содержит новых членов, мы не обращаемся к информации класса, мы используем только унаследованные элементы класса. Поэтому данное преобразование на уровне машинного кода — тождественно (т.е. вообще отсутствует). И никакие изменения в TSomeClass не приведут к поломке этого кода.

Разумеется, если у вас нет контроля над TSomeClass , то вы не сможете изменить область видимости FItem . В этом случае вы, конечно, можете использовать уже грязный хак (который является довольно опасным, т.к. произвольные изменения исходного класса могут его поломать). Но это не лучший вариант из-за опасной природы хака.

Расширение дружественности: интерфейсы

Итак, возвращаясь к нашим баранам, вариант второй разрешения конфликта — использование интерфейсов. Интерфейс (в смысле конструкции языка interface ) — это набор методов (читай: действий), которые можно произвести с объектом. Иными словами, это как бы «копия» public секции объекта. Вкусность тут в том, что их может быть много у одного объекта. Любой внешний код может запросить у объекта любой его сервис, если он знает его «имя» (в терминах интерфейсов: имя = GUID).

Таким образом, класс TSomeClass может реализовывать один или несколько интерфейсов, предназначенных для его дружественных классов, расположенных в других модулях. Чтобы подчеркнуть внутреннюю направленность интерфейсов, их можно вынести в отдельный недокументированный модуль. Тогда TAnotherClass может запросить у объекта класса TSomeClass интерфейс для внутренних манипуляций:
Как видите, вариант с интерфейсами — это гарантировано чистый путь, но неприятен он именно необходимостью смешивать объекты и интерфейсы (т.е. сущности с разным принципом управления временем жизни) в рамках одного класса. Как правило, чистота этого подхода не перевешивает его сложности по сравнению с чистым хаком, описанным выше.

Множественное наследование и интерфейсы

Но здесь, однако, кроется интересное для меня наблюдение. Если вы заметили, то объекты хорошо позволяют «реализовать что-то». У них есть наследование (и интерфейса и реализации), скрытие и дружественность, и т.д. Всё это позволяет легко реализовывать функциональность. Но не всегда это идеально удобно с точки зрения потребления/использования. Здесь уже ограничители (наследование, скрытие, . ) начинают скорее мешаться, чем приносить пользу. В самом деле, если я хочу добавить элемент в коллекцию, зачем мне всенепременно наследовать его от нужного класса? А если мне нужно добавить в коллекцию класс вне её дерева наследования? Ведь, по сути, здесь было бы достаточным, чтобы элемент поддерживал определённую функциональность (читай: «набор методов управления»). Для этого вовсе не обязательно строго наследовать от предопределённого класса, поскольку от элемента требуется лишь удовлетворять набору условий.

Насколько я понимаю, для решения этой проблемы (в том числе) в других языках программирования вводится поддержка множественного наследования: один класс может удовлетворять сразу нескольким функциональностям. К примеру, студент может быть не только человеком, но и рабочим, и музыкантом, и членом семьи (т.е. наследоваться от них в терминах множественного наследования). При этом, если наследуемые классы, в свою очередь, также наследуются друг от друга в различных комбинациях, то могут возникать конфликты (например, если «музыкант» и «член семьи» наследуют «прозвище» от «человек» с замещением реализации, то какую из реализаций должен брать «студент»?). Само собой, языки с поддержкой множественного наследования также содержат механизмы разрешения таких конфликтов. В целом же, множественное наследование часто критикуется. Проблемы, его чрезмерная сложность приводят критиков к выводу, что от него больше вреда, чем пользы. При этом также звучит мнение, что множественное наследование (как расширение «простого» одиночного наследования) — ошибочная концепция, порождённая неверным анализом и проектированием.

В Delphi проблемы множественного наследования решаются интерфейсами. Интерфейс — это полностью абстрактный класс, все методы которого виртуальны и абстрактны. Иными словами, говоря грубо, интерфейс — это запись ( record ) с указателями на функции. Каждый метод интерфейса должен быть реализован в классе. Причём реализацию вы либо пишете сами, либо наследуете, либо делегируете (для случая агрегации). Иными словами, любой интерфейс реализуется классом без конфликтов. Плюс, в каждый момент времени вы работаете с одним конкретным интерфейсом, а не с «объединённым набором интерфейсов», поэтому проблемы выбора нужной реализации тут просто нет — будет использоваться метод используемого интерфейса. Не та реализация? Берём другой (нужный) интерфейс с нужной реализацией.

Также замечу, что в Delphi отсутствует возможность множественного наследования не только классов, но и интерфейсов (множественное наследование интерфейсов существенно проще множественного наследования классов, поскольку интерфейсы не содержат реализации; тем не менее, даже здесь есть одна возможность для конфликтов).

В целом же, я считаю, что использование интерфейсов — это наиболее правильный вариант реализации множественного наследования, поскольку явное указание реализации исключает конфликты. Введение понятия интерфейсов является компромиссом, позволяющим получить преимущества множественного наследования, не реализуя его в полном объёме и, таким образом, не сталкиваясь со специфичными для него сложностями. Именно такой подход принят во многих современных языках — не только в Delphi, но и, к примеру, C# или Java.

Вы всё ещё используете объекты? Тогда мы идём к вам

Я уже кратко выразил основную мысль выше: объекты удобны для реализации функциональности (наследование/полиморфизм), но неудобны для использования. Добавлю только, что, помимо уже упомянутых ограничений, объекты неудобны ручным слежением за временем жизни. Интерфейсы же относятся к типам с автоматическим управлением временем жизни, что существенно упрощает разработку кода. Кроме того, интерфейсы — языко-независимы (т.е. ими можно обмениваться между кодом, написанных на разных языках), в отличие от классов, реализация которых своя в каждом языке программирования.

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

Иными словами, у интерфейсов есть куча плюсов, но есть и некоторые минусы. Во-первых, использование интерфейсов подвержено проблеме циклических ссылок (которую, впрочем можно легко обойти, следуя нескольким простым правилам разработки). Во-вторых, большая часть кода VCL и RTL написана в те времена, когда никакой поддержки интерфейсов в Delphi не было (историческая справка: изначально интерфейсы были введены в Delphi для поддержки технологии COM, но впоследствии их стали использовать более широко). Соответственно, весь этот код написан на объектах. И он наследуется и в современные версии Delphi. Более того, такой подход используют и сторонние библиотеки, руководствуясь «ну раз так поступает сам разработчик среды, то и мы тоже будем так делать». Итого, у вас может быть проблема состыковки кода с ручным и автоматическим управлением временем жизни. К сожалению, обычные интерфейсы в Delphi нельзя сделать «чистыми» (т.е. без обвеса автоматического управления). Вы можете добиться этого обходным путём, но это неудобно без поддержки со стороны языка.

Некоторые считают, что уже упомянутые плюсы интерфейсов не перевешивают их минусы. Возможно это и так. Но у интерфейсов есть одно существенное свойство, к которому я подвожу: интерфейс позволяет описывать некоторые желательные свойства, которыми могут обладать сущности. Интерфейс, по сути, является не только синтаксической, но и семантической конструкцией, используемой для спецификации услуг, предоставляемых компонентом. Интерфейс определяет границу взаимодействия между компонентами, определяя определённую абстракцию, которую осуществляет реализующая сторона. Интерфейс — это «договор», который обязуется выполнить компонент.

К чему это я? Когда мы говорим про ООП — мы говорим в первую очередь про наследование. Это именно то свойство (из наследования, абстракции, инкапсуляции и полиморфизма), которое наиболее ярко определяет ООП. Когда нас учат ООП, нас учат моделировать мир иерархиями объектов: объекты в программе отражают сущности реального мира. Но в реальном мире не существует понятия наследования (кроме нескольких узких областей). В реальной жизни стул не является наследником мебели. И уж тем более студент не является множественным наследником «человек», «рабочий», «музыкант».

Я не говорю, что наследование — это зло. Это удобная концепция, лучше всего раскрывающаяся для повторного использования кода (если у вас есть класс, вы можете наследоваться от него, чтобы сохранить его функциональность и внести некоторые изменения). Впрочем, при необходимости в качестве альтернативы наследованию можно выбрать агрегацию/делегирование.

Проблема в том, что унаследовавшись от одного предка, класс уже не может наследоваться от других. Изменение предка становится опасным. Зачастую правильное использование private и protected требует от программиста неслабых телепатических способностей: что может понадобится нашим наследникам, а что нет?

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

Если же вместо объектов вы будете оперировать интерфейсами, то ваш код будет более приближен к реальному миру. У вас может быть «студент», и он не будет наследоваться от «человека», но он будет иметь имя, т.к. студент одновременно является и «человеком». И если кому-то нужен «студент», то ему совершенно не обязательно наследоваться от «человека», «млекопитающего» или ещё более низкого класса, если его всего-лишь интересуют университет, курс и группа «студента». Конечно, вы можете и должны использовать объекты и наследование при реализации интерфейсов: в конце концов, наследование — удобный способ повторного использования кода. Иными словами, суммируя мысль: объекты — язык описания реализации, интерфейсы — язык описания реального мира.

Бонус: секция published

В заключение — несколько слов о секции published . Эта секция введена в Delphi для работы встроенного механизма сериализации (к примеру, именно благодаря ему загружаются формы из ресурсов программы). Помещение свойств, методов и полей в секцию published заставит компилятор генерировать мета-информацию (RTTI) для них, что позволит объекту «узнавать о самом себе» во время выполнения. Именно это (генерация RTTI) — основное назначение секции published . Но кроме этого секция published также имеет ту же область видимости, что и public . К примеру, все компоненты любой формы доступны любому коду, т.к. находятся в секции published , т.е. имеют видимость public . Конечно же, это нарушает инкапсуляцию (концепцию «чёрного ящика»). Любой код может проводить произвольные манипуляции с формой, даже приводя её в недопустимое состояние (к примеру, отключив все кнопки на форме).

Поэтому нужно всегда помнить о назначении секции published (генерировать RTTI) и не пытаться манипулировать компонентами формы в обход её публичного интерфейса. С этой точки зрения дизайн Delphi был бы более удачен, если бы секция published имела бы ту же область видимости, что и protected . В настоящее же время наилучшим вариантом будет вообще скрыть форму за фасадом. Например — интерфейсом. или: Подобный подход не только прост в реализации, но и добавляет в код высокую степень полиморфизма: в любой момент вы можете заменить форму на консольный ввод или ответ от удалённого сервера. Для этого достаточно будет заменить реализацию IInputDialog . Любой иной код, который его использует, совершенно не изменится.

Предложение Delphi ‘private’ (директива) не работает

Я пытаюсь проверить, действительно ли мои частные процедуры являются частными. Но это работает так, как должно.

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

Этот код не должен работать. Я полагаю. Но он работает.

Спасибо. (Delphi-7, Win7 x64).

private для a unit .

В более поздней версии Delphi вы можете использовать strict private для получения ожидаемого поведения.

Значение private явно документировано:

Частные, защищенные и общедоступные элементы

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

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

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

Строгие спецификации видимости

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

Доступны только члены класса с видимой видимостью строгое закрытие. в классе, в котором они объявлены. Они не видны процедур или функций, объявленных в одном блоке. Члены класса с видимостью строго охраняемой видимы внутри класса, в котором они объявляются и в пределах любого класса потомков, независимо от где он объявлен. Кроме того, когда члены экземпляра (те объявленные без класса или класса var) объявляются strict private или строго защищены, они недоступны вне экземпляр класса, в котором они появляются. Экземпляр класса не могут получить доступ к строго защищенным или строго защищенным членам экземпляра в другие экземпляры одного и того же класса.

Традиционный частный спецификатор видимости Delphi сопоставляется с CLR видимость сборки. Спецификатор видимости защищенный. сборку CLR или семейную видимость.

Примечание: слово strict рассматривается как директива в контексте объявления класса. В объявлении класса вы не можете объявить член, названный «строгим», но приемлемый для использования за пределами объявление класса.

Ваша версия Delphi, Delphi 7, не поддерживает спецификаторы strict .

Области видимости

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

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

В модели объектов языка Object Pascal существует механизм доступа к составным частям объекта, определяющий области, где ими можно пользоваться (области видимости). Поля и методы могут относиться к четырем группам (секциям), отличающимся областями видимости. Методы и свойства могут быть общими (секция public ), личными (секция private ), защищенными (секция protected ) и опубликованными (секция published). Есть еще и пятая группа, automated , она ранее использовалась для создания объектов СОМ; теперь она присутствует в языке только для обратной совместимости с программами на Delphi версий 3—5.

Области видимости, определяемые первыми тремя директивами, таковы.

  • Поля, свойства и методы секции public не имеют ограничений на видимость. Они доступны из других функций и методов объектов как в данном модуле, так и во всех прочих, ссылающихся на него.
  • Поля, свойства и методы, находящиеся в секции private, доступны только в методах класса и в функциях, содержащихся в том же модуле, что и описываемый класс. Такая директива позволяет полностью скрыть детали внутренней реализации класса. Свойства и методы из секции private можно изменять, и это не будет сказываться на программах, работающих с объектами этого класса. Единственный способ для кого-то другого обратиться к ним — переписать заново созданный вами модуль (если, конечно, доступны исходные тексты).
  • Поля, свойства и методы секции protected также доступны только внутри модуля с описываемым классом. Но — и это главное — они доступны в классах, являющихся потомками данного класса, в том числе и в других модулях. Такие элементы особенно необходимы для разработчиков новых компонентов — потомков уже существующих. Оставляя свободу модернизации класса, они все же скрывают детали реализации от того, кто только пользуется объектами этого класса.

Рассмотрим пример, иллюстрирующий три варианта областей видимости.

Листинг 1.1. Пример задания областей видимости методов

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

Для создания нового класса можно выделить 4 случая:

  1. Создание Windows-элемента управления (TWinControl)
  2. Создание графического элемента управления (TGraphicControl)
  3. Создание нового класса или элемента управления (TCustomControl)
  4. Создание невизуального компонента (не видимого) (TComponent)

Теперь попробую объяснить что же такое визуальные и невизуальные компоненты. Визуальные компоненты видны во время работы приложения, с ними напрямую может взаимодействовать пользователь, например кнопка Button — является визуальным компонентом.

Невизуальные компоненты видны только во время разработки приложения (Design-Time), а во время работы приложения (Run-Time) их не видно, но они могут выполнять какую-нибудь работу. Наиболее часто используемый невизуальный компонент — это Timer.

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

Шаг 2. Создание пустого модуля компонента

Чтобы приступить к непосредственному написанию компонента, вам необходимо сделать следующее:

  • Закройте проекты, которые вы разрабатывали (формы и модули)
  • В основном меню выберите Component -> New Component.
  • Перед вами откроется диалоговое окно с названием «New Component»
  • В поле Ancestor Type (тип предка) выберите класс компонента, который вы хотите модифицировать. В нашем случае вам надо выбрать класс TButton
  • В поле Class Name введите имя класса, который вы хотите получить. Имя обязательно должно начинаться с буквы «T». Мы напишем туда, например, TCountBtn
  • В поле Palette Page укажите имя закладки на которой этот компонент появиться после установки. Введем туда MyComponents (теперь у вас в Делфьи будет своя закладка с компонентами!).
  • Поле Unit File Name заполняется автоматически, в зависимости от выбранного имени компонента. Это путь куда будет сохранен ваш модуль.
  • В поле Search Path ничего изменять не нужно.
  • Теперь нажмите на кнопку Create Unit и получите следующее:

Шаг 3. Начинаем разбираться во всех директивах

Что же здесь написано? да собственно пока ничего интересного. Здесь объявлен новый класс TCountBtn и процедура регистрации вашего компонента в палитре компонентов.

Директива Private. Здесь вы будете писать все скрытые поля которые вам понадобятся для создания компонента. Так же в этой директиве описываются процедуры и функции, необходимые для работы своего компонента, эти процедуры и функции пользователю не доступны. Для нашего компонент мы напишем туда следующее (запись должна состоять из буквы «F» имени поля: тип этого поля): Буква «F» должна присутсвовать обязательно. Здесь мы создали скрытое поле Count, в котором и будет храниться число кликов по кнопке.

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

Это указывает на то, что мы будем обрабатывать щелчок мыши по компоненту. Слово «override» указывает на то, что мы перекроем стандартное событие OnClick для компонента предка.

В директиве Public описываются те процедуры и функции компонента, которые будут доступны пользователю. (Например, в процессе написания кода вы пишите имя компонента, ставите точку и перед вами список доступных функций, объявленных в диретиве Public). Для нашего компонента, чтобы показать принцип использования этой директивы создадим функцию — ShowCount, которая покажет сообщение, уведомляя пользователя сколько раз он уже нажал на кнопку. Для этого в директиве Public напишем такой код: Осталась последняя директива Published. В ней также используется объявления доступных пользователю, свойств и методов компонента. Для того, чтобы наш компонент появился на форме необходимо описать метод создания компонента (конструктор), можно прописать и деструктор, но это не обязательно. Следует обратить внимание на то, что если вы хотите, чтобы какие-то свойства вашего компонента появились в Инспекторе Объектов (Object Inspector) вам необходимо описать эти свойства в директиве Published. Это делается так: property Имя_свойства (но помните здесь букву «F» уже не нужно писать), затем ставиться двоеточие «:» тип свойства, read процедура для чтения значения, write функция для записи значения;. Но похоже это все сильно запутано. Посмотрите, что нужно написать для нашего компонента и все поймете: Итак все объявления сделаны и мы можем приступить к написанию непосредственно всех объявленных процедур.

Шаг 4. Пишем процедуры и функции.

Теперь мы напишем процедуру обработки щелчка мышкой по кнопке: «Inherited click» означает, что мы повторяем стандартные методы обработки щелчка мышью (зачем напрягаться и делать лишнюю работу:)).

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

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

Шаг 5. Устанавливаем компонент

Если вы сумели написать и понять, все то что здесь предложено, то установка компонента не должна вызвать у вас никаких проблем. Все здесь делается очень просто. В главном меню выберите пункт Component -> Install Component. перед вами открылось диалоговое окно Install Component. В нем вы увидите две закладки: Into exsisting Package и Into new Package. Вам предоставляется выбор установить ваш компонент в уже существующий пакет или в новый пакет соответственно. Мы выберем в уже существующий пакет.

В поле Unit File Name напишите имя вашего сохранненого модуля (естественно необходимо еще и указать путь к нему), а лучше воспользуйтесь кнопкой Browse и выберите ваш файл в открывшемся окне.

В Search Path ничего изменять не нужно, Делфьи сама за вас все туда добавит.

В поле Package File Name выберите имя пакета, в который будет установлен ваш компонент. Мы согласимся с предложенным по умолчанию пакетом.

Теперь нажимаем кнопочку Ok. И тут появиться предупреждение Package dclusr30.dpk will be rebuilt. Continue? Дельфи спрашивает: «Пакет такой-то будет изменен. Продолжить?». Конечно же надо ответить «Да». И если вы все сделали правильно, то появиться сообщение, что ваш компонент установлен. Что ж можно кричать Ура! Это ваш первый компонент.

Создание свойств своего типа

Для начале нам необходимо объявить новый тип. Описание типа нужно добавить после слова Type. Вот так это выглядело вначале: Вот так это должно выглядеть: Здесь мы объявили новый тип TShowTp, который может принимать только два значения. Все значения, которые вы хотите добавить перечисляются через запятую.

Теперь нам понадобиться создать поле этого типа. Это мы уже умеем и делать и поэтому не должно вызвать никаких сложностей. В директиву Private напишите: Мы создали поле ShowType, типа TShowTp.

Конечно же необходимо добавить это свойство в инспектор объектов: Ну и наконец, чтобы наш компонент реагировал на изменение этого свойства пользователем надо слегка изменить обработчик события OnClick. После небольшой модификации он может иметь примерно такой вид: Объясню что произошло. Вначале мы увеличиваем счетчик на единицу. Затем проверяем какое значение имеет свойство ShowType. Если Normal, то ничего не делаем, а если CountToCaption, то в надпись на кнопке выводим количество кликов. Не так уж и сложно как это могло показаться с первого раза.

Имплантируем таймер в компонент

После раздела uses, где описаны добавленные в программу модули, объявите переменную типа TTimer. Назовем ее Timer. Приведу небольшой участок кода: Дальше в директиву Protected необходимо добавить обработчик события OnTimer для нашего таймера. Это делается так: Поскольку наш таймер это не переменная, а компонент, его тоже надо создать, для этого в конструктор нашей кнопки напишем: Здесь создается экземпляр нашего таймера и его свойству Iterval (измеряется в миллисекундах) присваивается значение 10000 (то есть 10 секунд если по простому).
Собственно осталось написать саму процедуру OnTimer. Я сделал это так:

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

Переустановка компонента

Очень часто бывает необходимо переустановить ваш компонент. Если вы попробуете сделать это путем выбора Component->Install Component, то Дельфи вас честно предупредит о том, что пакет уже содержит модуль с таким именем. Перед вами открывается окно с содержимым пакета. В нем вы должны найти имя вашего компонента и удалить его (либо нажать кнопочку Remove). Теперь в пакете уже нет вашего компонента. Затем проделайте стандартную процедуру по установке компонента.

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

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

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

Public — Директива 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 байта памяти для любого класса. Очевидно, что этого явно недостаточно для хранения всех переменных в классе. Размер этой переменной говорит о том, что в ней хранится указатель. Экземпляр (или рабочая копия) класса создается посредством вызова его конструктора:

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

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.

Илон Маск рекомендует:  Express, Node-фреймворк
Понравилась статья? Поделиться с друзьями:
Кодинг, CSS и SQL