Свойства классов в Delphi
Апрель 21st, 2013
admin
Поля данных целесообразно защитить от несанкционированного доступа по принципу инкапсуляции. Доступ к полям реализуется через свойства, в которые входят метод чтения, и метод записи полей. Исходя из этого, поля объявляются в разделе private (закрытый раздел класса). Для реализации доступа потомков данного класса иногда лучше объявить поля в защищенном разделе – protected. Важно: в большинстве случаев идентификаторы полей могут совпадать с именами соответствующих свойств, где добавляется префикс (символ) – «F».
Property <имя свойства>: <тип> read <имя поля или метода чтения>
write <имя поля или метода чтения>
<директивы запоминания>;
При прямом чтении или записи данных в разделе read или write записывается имя поля. По умолчанию директива запоминания имеет вид: default <сохранять значение по умолчанию, если не изменено пользователем>. Которое не задает начальные условия.
Урок 60 — Свойства классов Delphi, (часть 2/5)
В этом уроке мы продолжаем изучать классы и в этом уроке мы разберёмся в их свойствах.
Поля данных, исходя из принципа инкапсуляции — одного из основополагающих в объектно-ориентированном программировании, всегда должны быть защищены от несанкционированного доступа. Доступ к ним, как правило, должен осуществляться только через свойства, включающие методы чтения и записи полей. Поэтому поля целесообразно объявлять в разделе private — закрытом разделе класса. В редких случаях их можно помещать в protected — защищенном разделе класса, чтобы возможные потомки данного класса имели к ним доступ. Традиционно идентификаторы полей совпадают с именами соответствующих свойств, но с добавлением в качестве префикса символа F.
Свойство объявляется оператором вида:
Если в разделах read или write этого объявления записано имя поля, значит предполагается прямое чтение или запись данных (т.е. обмен данными непосредственно с полем).
Если в разделе read записано имя метода чтения, то чтение будет осуществляться только функцией с этим именем. Функция чтения — это функция без параметра, возвращающее значение того типа, который объявлен для свойства. Имя функции чтения принято начинать с префикса Get, после которого следует имя свойства.
Если в разделе write записано имя метода записи, то запись будет осуществляться только процедурой с этим именем. Процедура записи — это процедура с одним параметром того типа, который объявлен для свойства. Имя процедуры записи принято начинать с префикса Set, после которого следует имя свойства.
Если раздел write отсутствует в объявлении свойства, значит это свойство только для чтения и пользователь не может задавать его значение.
Директивы запоминания определяют, как надо сохранять значения свойств при сохранении пользователем файла формы .dfm. Чаще всего используется директива default — значение по умолчанию.
Она не задает начальные условия. Это дело конструктора. Директива просто говорит, что если пользователь в процессе проектирования не изменил значение свойства но умолчанию, то сохранять значение свойства не надо.
Начните новый проект. Объявление класса, который мы хотим создать, можно поместить непосредственно в файл модуля. Но если вы хотите создать класс, который будете использовать в различных проектах, лучше оформить его в виде отдельного модуля unit, сохранить в каталоге своей библиотеки или библиотеки Delphi, и подключать в дальнейшем к различным проектам с помощью предложения uses. Мы выберем именно этот вариант. Так что выполните команду File | New | Unit (в некоторых более старых версиях Delphi — File | New и на странице New выберите пиктограмму Unit). Сохраните сразу этот ваш модуль в библиотеке под именем, например, MyClasses. А в модуль формы введите оператор, ссылающийся на этот модуль:
Теперь займемся созданием класса в модуле MyClasses. Текст этого модуля может иметь пока такой вид:
Вглядимся в приведенный код. Интерфейсный раздел модуля interface начинается с предложения uses. Заранее включать это предложение в модуль не требуется. Но по мере написания кода вы будете встречаться с сообщениями компилятора о неизвестных ему идентификаторах функций, типов и т.п. Столкнувшись с таким сообщением, надо посмотреть во встроенной справке Delphi или в справке [3], в каком модуле объявлена соответствующая функция или класс. И включить этот модуль в приложение uses.
Теперь обратимся к объявлению класса. Объявленный класс TPerson наследует непосредственно классу TObject, поскольку родительский класс не указан. В закрытом разделе класса private объявлен ряд полей. Поле FName предполагается использовать для фамилии, имени и отчества человека. Поля FDepl, FDep2, FDep3 будут использоваться под указание места работы или учебы. Поле FYear будет хранить год рождения, поле FSex — указание пола: символ «м» или «ж». Поле FAttr будет хранить какую-то характеристику: штатный — нештатный, отличник или нет и т.п. Поле FComment предназначено для каких-то текстовых комментариев. В частности, в нем можно хранить свойство Text многострочного окна редактирования Memo или RichEdit. Так что это может быть развернутая характеристика человека, правда, без форматирования.
В открытом разделе класса public объявлены свойства, соответствующие всем полям. Чтение всех свойств осуществляется непосредственно из полей. Запись во всех свойствах, кроме Sex, осуществляется тоже непосредственно в поля. А для поля Sex указана в объявлении свойства процедура записи SetSex, поскольку надо следить, чтобы по ошибке в это поле не записали символ, отличный от «м» и «ж». Соответственно в защищенном разделе класса protected содержится объявление этой процедуры. Как говорилось ранее, она должна принимать единственный параметр типа, совпадающего с типом свойства.
В раздел модуля implementation введена реализация процедуры записи SetSex. Ее заголовок повторяет объявление, но перед именем процедуры вводится ссылка на класс TPerson, к которому она относится. Не забывайте давать такие ссылки для методов класса. Иначе получите сообщение компилятора об ошибке; Unsatisfied forward or external declaration: TPerson.SetSex — нереализованная ранее объявленная или внешняя функция TPerson.SetSex.
Тело процедуры SetSex в особых комментариях не нуждается. В нем проверяется допустимость символа, переданного в процедуру через параметр Value. Если символ допустимый, то его значение заносится в поле FSex. Это поле, как и другие закрытые поля, открыто для методов данного класса. При ошибочном символе пользователю выдается сообщение об ошибке. Правда, лучше было бы в этом случае сгенерировать исключение, но пока мы не обсуждали, как это можно делать.
Вы создали класс в вашем модуле MyClasses. Давайте посмотрим, как можно использовать объекты нашего класса. Создайте в модуле формы Unit1 вашего приложения тест класса TPerson. Введите в модуль операторы:
uses Classl;
var Pers: TPerson;
Они обеспечивают связь с модулем, описывающим класс, и объявляют переменную Pers, через которую вы будете связываться с объектом класса. Но так же, как при работе с другими объектами и записями, объявление этой переменной еще не создает сам объект. Это указатель на объект, и его значение равно nil.
Создание объекта вашего класса TPerson должно осуществляться вызовом его конструктора Create. Так что создайте обработчик события OnCreate вашей формы, и вставьте в него оператор:
Вот теперь объект создан, и переменная Pers указывает на него. Чтобы не забыть очистить память от этого объекта при завершении работы приложения, сразу создайте обработчик события OnDestroy формы, и вставьте в него оператор:
Почему ваш объект воспринимает методы Create и Free? Ведь вы их не объявляли в классе TPerson! Это работает механизм наследования. В классе TObject, являющемся родительским для TPerson, методы Create и Free имеются. А поскольку вы их не перегружали, то ваш класс наследует их.
Теперь перенесите на форму четыре окна Edit, окно Memo и две кнопки. Пусть первая кнопка с надписью Запись заносит в объект Pers данные из окон редактирования: ‘ фамилию с именем и отчеством, пол, подразделение, в котором работает или обучается человек, год рождения, характеристику из окна Memo. Обработчик щелчка на ней может иметь вид:
А вторая кнопка с надписью Чтение пусть осуществляет чтение информации из объекта в окна редактирования. Обработчик щелчка на ней может иметь вид:
Выполните ваше приложение. Занесите в окна редактирования какую-то подходящую информацию и щелкните на кнопке Запись. А потом сотрите тексты всех окон редактирования и щелкните на кнопке Чтение. Информация в окнах должна восстановиться. Проверьте также реакцию на неверный символ, задающий пол.
Delphi RTTI: получить класс свойства
Используя Delphi 2010 и RTTI, я знаю, как получить тип класса объекта и как получить/установить значение и тип свойств объекта, но как определить, из какого класса в цепочке наследования произошло свойство? Я хочу использовать свойства базового класса иначе, чем свойства основного класса.
Рассмотрим этот код:
Проблема в том, что для свойств нет GetClassType. ClassType просто возвращает TRttiInstancePropertyEx вместо имени класса, которому принадлежит свойство.
задан 05 июля ’12, 19:07
Ваш вопрос немного сбивает с толку. Пожалуйста, уточните. Что ИМЕННО вы ищете? Вы пытаетесь определить, Obj.PropertyName возвращает объект, который является TClassBase экземпляр по сравнению с TClassDescendant пример? Или вы пытаетесь определить, Obj.PropertyName сам объявляется как TClassBase независимо от того, какой тип класса реализует экземпляр возвращаемого объекта? Как TClassBase и TClassDescendant используется объектами, которые вы проверяете? — Remy Lebeau
Я хочу знать, «как вы определяете, из какого класса в цепочке наследования произошло свойство» или, скорее, это свойство в TClassBase или в TClassDescendant. Когда я просматриваю свойства класса, я хочу игнорировать свойства базового класса. В моей конкретной ситуации я наследовал класс от TInterfacedObject и выполняю функцию для всех свойств, если они не имеют атрибута [Ignore], но я также хочу легко игнорировать RefCount от TInterfacedObject. — David Cornelius
Вместо того, чтобы проверять, существует ли текущее свойство в определенном классе, было бы разумнее проверить, является ли перечисляемый объект предполагаемым классом или нет. Это было бы намного проще реализовать и было бы точнее. — Remy Lebeau
@RemyLebeau, нет, я использую TClassDescendant и просматриваю его свойства, но, поскольку он наследует TClassBase, он приносит с собой эти свойства. Я не знал, как определить, к какому классу принадлежит свойство, просматривая все свойства. Мне нужно было игнорировать все свойства, объявленные в TInterfacedObject. — David Cornelius
Как получить свойства класса в delphi
Современные программы, как правило, сложны и велики по объему исходного кода, и использования лишь обычных процедурных методов, описанных в первой части, недостаточно для написания полноценных Windows-приложений. Поэтому здесь мы рассмотрим объектно-ориентированное программирование, а так же остановимся на вопросах планирования программ и цикла их разработки.
Суть современного программирования
Если в 80-х годах повсеместно применялось, в основном, процедурное программирование, то с приходом в начале 90-х графических сред наподобие Windows, сопровождающееся значительным усложнением структуры и размеров программного кода, возник вопрос использования новых, более прогрессивных средств. Таковыми оказались объектно-ориентированные языки программирования — такие, как C++, а немного позже к ним добавились новые, например, язык Java или полностью переработанные старые — Object Pascal.
ПРИМЕЧАНИЕ
Собственно говоря, язык C++ так же является переработанным вариантом процедурного языка C, только появился он гораздо раньше — в 1982 году. Что касается языка Pascal, то в принципе, первой попыткой переложения его на объектно-ориентрованное применение можно считать такую ветвь, как язык Ada.
Увеличение сложности программ привело не только к появлению новых инструментов, упрощающих разработку, но и повлекло за собой кардинальной пересмотр путей подхода к решению поставленной задачи. Так, если во времена DOS, программист, получив задание, сразу приступал к кодированию, то теперь этому предшествует этап анализа и предварительной разработки. Более того, теперь существует несколько специализаций, причем не всех их можно назвать программистами в привычном смысле этого слова. Это системные аналитики, дизайнеры интерфейса, программные инженеры, собственно «кодеры» и другие. Впрочем, такое многообразие больше характерно для крупных коллективов, решающих объемные задачи. В более простом и прагматичном случае все эти обязанности выполняет один человек.
Итак, что же делать разработчику, получившему задачу по созданию того или иного приложения? Прежде всего, требуется проанализировать поставленную задачу, т.е. определить, какой вариант решения поставленного вопроса будет наиболее правильным, и что понадобится для его реализации. В простейшем случае понадобятся только компьютер и установленная на нем среда Delphi. Для решения более сложных задач могут понадобиться другие программы (начиная от приложений баз данных и заканчивая графическим редактором), а возможно — и другие специалисты, как-то другие программисты, 3D-моделисты, художники, технические писатели и т.д. На этом этапе так же важно взаимодействие с заказчиком проекта для внесения полной ясности в понимание того, чего все-таки требуется от программы, возможности внесения изменений, увеличения или уменьшения функциональности и т.п.
После того, как ситуация прояснится в достаточной степени для того, чтобы было понятно что и зачем надо делать, можно будет окончательно определится с тем, что понадобится для работы и составить общую схему программы. После этого можно перейти к детальной разработке программы.
На этапе детальной разработки следует перейти к определению конкретных составляющих программы — от общей структуры проекта до отдельных диалоговых окон, схемы поведения, вариантов вывода отчетов и т.д. Следует так же продумать реализацию тех или иных алгоритмов, структуру хранения данных и прочие подобные вопросы. Детальная проработка этих вопросов до того, как начнется собственно процесс написания программы, существенно ускорит весь процесс и, возможно, избавит от многократного переписывания целых фрагментов программы.
Наконец, когда ясны все цели поставленной задачи и намечены пути их решения, а так же есть ясность в том, что и как должно выглядеть и работать, можно приступать к написанию исходного кода — кодированию. И хотя это далеко не последний этап создания программы, процесс написания исходного кода, однажды начавшись, будет продолжаться во всех последующих этапах. А следующим этапом будет тестирование программы на работоспособность, выявление различных ошибок, доводке отдельных модулей и т.д. Причем чем больше программа по объему, тем дольше и сложнее ее тестировать, причем зависимость здесь отнюдь не линейная: увеличение размера программы вдвое увеличивает число потенциальных ошибок, как минимум, вчетверо.
Последним, и самым продолжительным этапом разработки, является сопровождение программы. Дело в том, что никакое тестирование не выловит все огрехи кода в большой программе. Поэтому выявление пользователями различных «недокументированных возможностей», а, попросту говоря — ошибок, может продолжаться ровно до тех пор, пока данное творение используется вообще. Кроме того, заказчику со временем наверняка захочется получить какие-либо дополнительные, ранее не предусмотренные возможности, изменить вид или тип поведения для тех или иных модулей и т.д.
Разумеется, для всего этого придется вновь и вновь возвращаться к написанию исходного кода. Таким образом, можно сказать, что отладка и сопровождение — поздние фазы этапа кодирования. В то же время, не будем забывать, что в целом программа готова уже после того, как закончился этап основного кодирования — именно там, где мы его определили, т.е. до начала тестирования. Но, опять-таки, тестирование тоже бывает разным — в практике выделяют несколько его фаз, а именно:
- Альфа-тестирование. Это предварительный вариант программы, на этапе выпуска альфа-версии функциональность продукта, как правило, не является полной;
- Бета-тестирование. После того, как с одной стороны, исправлены ошибки, выявленные при альфа-тестировании, а с другой — завершено функциональное наполнение продукта, выпускают бета-версию;
- Финишная подгонка, или Pre-Release. С некоторых пор вошло в моду предварять выход окончательной версии предварительным выпуском (PR, RC). На этот момент программа должна на 100% соответствовать тому, что ожидается в окончательной версии (Release), суть же выпуска такой версии — убедиться еще раз, что все в порядке.
Фактически, можно констатировать, что этап основного кодирования завершается одновременно с выпуском бета-версии, т.е. когда программа уже умеет делать все, что ей положено, возможно, за исключением тех случаев, когда она не может что-то сделать из-за не выявленной ранее ошибки.
Использование объектно-ориентированного подхода как раз и служит для того, чтобы разработчику было проще ориентироваться в исходном коде, и минимизировать, или даже локализовать допущенные при кодировании ошибки.
Классы и объекты
Основой объектно-ориентированного программирования являются сами объекты. Каждый объект обладает характеристиками (свойствами, атрибутами) и моделью поведения (методами, реакцией не события). К примеру, объект, описывающий машину, может иметь свойство, отвечающее за то, что с ней происходит. Допустим, машина может ехать или стоять на месте, соответственно для определения того, стоит машина на месте или движется, может отвечать некое свойство булевского типа.
В ООП описания объектов называются классами (class). Фактически, класс является дальнейшим развитием уже знакомого нам типа данных — записи. Но если запись имела только свойства (поля) и ничего более, то классы могут содержать все, что требуется для полноценного объекта. При этом класс может быть сколь угодно большим и иметь в качестве своих свойств другие классы. Например, класс «машина» может иметь в своем описании такие свойства, как двигатель, шасси и им подобные, представленные другими классами. При этом для каждого такого класса так же могут быть определены собственные свойства и методы.
Самими же объектами в программе называют не классы, а их экземпляры. Т.е. если мы имеем класс TCar, то его экземпляр, представленный в виде переменной типа TCar (скажем, MyCar), как раз и будет объектом. Чтобы использовать объект, его надо предварительно создать. Делается это при помощи специального метода, который определен для всех классов — Create, называемый так же конструктором класса. Соответственно, чтобы получить объект, являющийся экземпляром класса, мало объявить переменную — надо еще и проинициализировать его при помощи конструктора:
var MyCar: TCar; . MyCar.Create;
Разумеется, каждый класс может иметь множество своих экземпляров. Скажем, кроме объекта MyCar, могут быть еще BossCar, AnyCar, Truck23, Bus144 и т.д. Все эти переменные называются переменными-экземплярами класса.
Другой составляющей ООП являются 3 концепции, применяемые к классам — инкапсуляция, наследование и полиморфизм. И хотя в предыдущей части книги мы уже рассмотрели все эти термины, вспомним их суть здесь еще раз, причем уже применительно к практическому применению классов.
Так, инкапсуляция, или объединение в единое целое всего того, что относится к определенному объекту, на практике выливается в возможность объединить в одном объекте свойства и методы. Наследование позволяет, единожды создав некий обобщенный класс, пользоваться его свойствами и методами из других классов, созданных на его основе — в классах-потомках. Ну а применение полиморфизма на практике позволит переопределить свойства или изменить поведение отдельных методов родительского класса в этих самых классах-потомках.
В результате всего этого достигается более компактный и понятный код. В частности, благодаря инкапсуляции, для изменения состояния объекта достаточно обратиться к его же методу, а не использовать отдельную функцию. Наследование — это замечательная вещь для многократного использования единожды написанного кода. Ну а полиморфизм позволяет в родственных классах одинаково называть методы, отличающиеся в программной реализации, но одинаковые по своей сути.
Структура класса и видимость
Для объявления класса используют ключевое слово class. При этом, как и любые другие пользовательские типы, объявлению класса или классов всегда предшествует другое ключевое слово — type. Далее следует перечисление всех его полей, свойств и методов, сгруппированных по 4 разделам, определяющих уровень видимости. В результате для описания класса мы имеем следующую структуру:
type = class ( ) private protected public published end;
В представленной структуре описаниями являются объявления свойств и методов, при этом большинство составных частей в объявлении класса являются необязательными. В частности, если класс не имеет специфического предка, то имя родительского класса можно опустить, при этом подразумевается, что такой класс происходит от общего для всех классов предка — TObject. Что касается секций, определяющих уровень видимости, то их в ряде случаев так же можно опустить. При этом будет подразумеваться, что все объявленные в классе свойства и методы будут принадлежать к категории опубликованных (published). Таким образом, следующие объявления классов TClass1 и TClass2 будут полностью эквивалентны:
type TClass1 = class (TObject) published Property1: integer; end; type TClass2 = class Property1: integer; end;
Что касается области видимости, называемых так же правами доступа то, как следует из синтаксиса определения класса, их может быть 4 вида. Так, разделы private и protected содержат защищенные описания, а public и published — общедоступные. Более тонкие различия между этими категориями заключаются в следующем:
- Категория Private является наиболее защищенной. Доступ к помещенным в эту группу свойствам и методам можно получить только в том же самом модуле;
- В категорию Protected помещают те свойства класса, доступ к которым возможен из дочерних классов, объявленных так же и вне данного модуля;
- Раздел Public содержит свойства и методы, которые должны быть видны во всех местах программы, где используется данный класс, т.е. «публичные».
Что касается опубликованных (published) свойств, то эта категория специально создана для библиотеки VCL. Эти те самые свойства, которые отображаются в инспекторе объекта, в остальном они аналогичны публичным свойствам. Все свойства, категория которых в классе явно не задана, считаются опубликованными.
Теперь обратимся собственно к содержимому этих групп. В их качестве могут выступать поля — по аналогии с записями, а так же методы — подпрограммы (как функции, так и процедуры). При этом если имена классов принято начинать с буквы T, то имена полей обычно начинают с буквы F (field). Что касается названий методов, то для их наименования руководствуются теми же принципами, что и при написании обычных подпрограмм, т.е. дают название, характеризующее выполняемое данным методом действие. Для примера создаем класс TUser, содержащий имя учетной записи (login) и пароль (password), а так же некий метод, проверяющий имя и пароль:
type TUser = class fLogin: string; fPassword: string; function Connect: boolean; end;
При реализации метода — в данном случае функции Connect, следует указывать имя класса, к которому она принадлежит. Например, в нашем случае она может выглядеть следующим образом:
function TUser.Connect: boolean; var s,p: string; begin writeln(‘Input username:’); readln(s); writeln(‘Input password:’); readln(p); result:= (s=fLogin) and (p=fPassword); end;
Фактически, составление подпрограммы как метода класса, ничем не отличается от написания обычной процедуры или функции, с той лишь разницей, что перед ее названием указывается имя класса, а в самой функции в качестве переменных доступны все поля этого класса — в данном случае fLogin и fPassword.
Кроме полей и методов, существуют собственно свойства (properties), являющиеся чем-то средним: они могут использовать, скажем, поле, для хранения значения, и функцию для его изменения. Фактически, свойства реализуют механизм доступа к полям. В таком случае поля, как правила, делают защищенными (private), а свойства — общедоступными (published). Допустим, в нашем классе TUser, который хранит некую информацию о пользователе, можно реализовать процедуру смены пароля таким образом, чтобы при попытке установки нового пароля он проверялся на допустимость — скажем, подходил по количеству знаков. В таком случае можно создать свойство Password, которое будет обращаться непосредственно к полю fPassword в случае, когда пароль надо просто считать, и вызывать процедуру проверки при попытке изменения этого поля. Попутно создадим свойство Login, но без процедуры проверки. Объявление такого класса может выглядеть следующим образом:
type TUser = class private fLogin: string; fPassword: string; procedure setPassword(newpass: string); published property Login: read fLogin write fLogin; property Password: read fPassword write setPassword; function Connect: boolean; end;
Обратите внимание, что поля теперь вынесены в защищенную часть класса, там же определена и процедура проверки пароля. Что касается свойств и метода Connect, то они находятся в общедоступной секции.
При обращении к свойству Password на чтение (read) будет автоматически возвращаться значение, хранящееся в поле fPassword, а при попытке записать (write) в поле новое значение будет вызываться процедура setPassword. Сама эта процедура может выглядеть следующим образом:
procedure TUser.setPassword(newpass: string); begin if Length(newpass)>3 then fPassword:=newpass else writeln(‘Error! Password is too short!’); end;
Эта подпрограмма проверяет новое значение пароля на длину, и если оно содержит более 3 символов, то присваивается полю fPassword, в противном случае значение поля не меняется, и выводится сообщение об ошибке. В самой же программе обращение к свойству класса выглядит точно так же, как к полю записи, т.е. с использованием доступа через точку:
Пример программы, использующей класс TUser, включая описание самого класса вместе со всеми его полями, свойствами и методами, представлен в листинге 7.1 (см. так же Demo\Part2\ClassProp).
Листинг 7.1. Класс со свойствами
program classprop; <$APPTYPE CONSOLE>type TUser = class private fLogin: string; fPassword: string; procedure setPassword(newpass: string); published property Login: string read fLogin write fLogin; property Password: string read fPassword write setPassword; function Connect: boolean; end; procedure TUser.setPassword(newpass: string); begin if Length(newpass)>3 then fPassword:=newpass else writeln(‘Error! Password is too short!’); end; function TUser.Connect:boolean; var s,p: string; begin writeln(‘Input username:’); readln(s); writeln(‘Input password:’); readln(p); result:= (s=fLogin) and (p=fPassword); end; var User: TUser; s,p: string; begin User:=TUser.Create; writeln(‘Create username’); readln(s); User.Login:=s; writeln(‘Create password’); readln(p); User.Password:=p; writeln(‘Username is: ‘+User.Login); writeln(‘Password is: ‘+User.Password); writeln(‘Try to login. ‘); while User.Password<>» do begin if User.Connect then begin writeln(‘All Ok.’); break; end else writeln(‘Incorrect data. Try again!’); end; User.Destroy; readln; end.
В начале здесь создается объект User, являющийся экземпляром класса TUser, после чего его полям fLogin и fPassword, посредством соответствующих свойств назначаются значения. При этом неявно задействуются методы write этих свойств. После чего данные считываются, опять-таки через свойства Login и Password, на сей раз будут использованы методы read. После этого, если пароль был задан, начинается цикл, для выхода из которого пользователю надо ввести верные имя и пароль, для чего использован метод Connect.
Обратите в этом листинге внимание на то, что в подпрограммах, являющихся членами класса, напрямую используются поля, а вне класса задействуются свойства. Хотя в данном случае это и не обязательно, поскольку все находится в одном модуле, а защищенные поля видны в нем повсеместно, в основном теле программы обращения идут только к опубликованным свойствам и методам, что являются более типичным вариантом для реальных приложений.
Конструкторы и деструкторы
В самом начале предыдущего примера мы использовали метод Create для создания экземпляра класса TUser, а в конце — метод Destroy для его удаления, хотя сами эти методы определены в классе не были. Но вспомним, что в ООП существует такое понятие, как наследование, а все классы в Delphi происходят от класса TObject. Именно по наследству от TObject классу TUser эти методы и достались.
Метод Create является конструктором класса, т.е. подпрограммой, создающий объект (экземпляр класса) в памяти. В том случае, если при создании экземпляра класса никаких дополнительных действий производить не требуется, используют метод Create, доставшийся от родительского класса. Если же при создании объекта требуется выполнить какие-либо действия, то метод Create переписывают заново. Например, если при инициализации класса TUser нам надо было бы задать какие-либо имя пользователя и пароль по умолчанию, то мы могли бы определить конструктор заново:
type TUser = class . constructor Create; end; constructor TUser.Create; begin inherited Create; fLogin:=’User’; fPassword:=’password’; end;
Обратите внимание на то, что, во-первых, конструктор — это не процедура и не функция, а именно конструктор, и задается, соответственно, при помощи ключевого слова constructor, а во-вторых, в самом теле подпрограммы-конструктора первым делом производится вызов родительского конструктора при помощи ключевого слова inherited. Таким образом, унаследованный метод выполняет всю необходимую для начальной инициализации объекта работу. Как нетрудно догадаться, это возможно благодаря такой концепции ООП, как полиморфизм.
Есть всего одно условие, касающееся расположения конструкторов и деструкторов в объявлении класса: они должны быть объявлены в общедоступной части, т.е. в public или в published.
Использование собственного конструктора необходимо в том случае, если объект в качестве своих полей содержит другие объекты. В таком случае следует написать собственный конструктор, который будет создавать эти объекты. Например, если в классе TUser было определено поле MegaInfo, являющееся объектом типа TMegaUserInfo, то конструктор класса TUser должен создавать соответствующий объект, вызывая его конструктор:
constructor TUser.Create; begin inherited Create; MegaInfo:=TMegaUserInfo.Create; . end; Что касается метода Destroy, то он призван освободить память, занимаемую экземпляром класса, и называется деструктором (destructor). Соответственно, в том случае, если среди полей класса будут другие объекты, использование переопределенного деструктора обязательно. Например, для класса TUser с полем типа TMegaUserInfo мы получим следующий деструктор: destructor TUser.Destroy; begin . MegaInfo.Destroy; inherited Destroy; end;
Обратите внимание на то, что если в конструкторе наследуемый метод Create всегда вызывается первым, то в деструкторе наследованный метод Destroy — последним.
Однако просматривая исходные коды других программ (включая библиотеку VCL) вы вряд ли обнаружите вызов метода Destroy напрямую. Дело в том, что к моменту уничтожения того же объекта TUser поле MegaInfo уже может быть освобождено, например, путем присвоения ему в программе значения «nil» (нулевого указателя):
User.MegaInfo:=nil; User.Destroy; // здесь во время выполнения возникнет ошибка! Таким образом, попытка повторно освободить память при помощи метода Destroy приведет к ошибке выполнения программы. Поэтому на практике, вместо метода Destroy всегда используют метод Free, который сначала проверяет, не является ли ссылка на объект нулевым указателем, и если нет — то только тогда вызывает деструктор. Соответственно, вид деструктора и его вызов из программы будут выглядеть таким вот образом: destructor TUser.Destroy; begin . MegaInfo.Free; inherited Destroy; end; . User.MegaInfo:=nil; User.Free; // теперь ошибки не будет
Таким образом, использование метода Free является прекрасной возможностью избежать непредвиденных ошибок времени выполнения.
Конструкторы и деструкторы, подобно другим подпрограммам, могут принимать значения в качестве параметров. Например, если при создании нового экземпляра класса TUser нам нужно было бы сразу получить его имя, то конструктор получился бы таким:
constructor TUser.Create(str: string); begin inherited Create; fLogin:=str; fPassword:=’password’; end;
Соответственно, создание объекта типа TUser в программе теперь будет выглядеть следующим образом:
Аналогичным образом параметры можно передавать и деструкторам, при этом, в зависимости от значения параметров, они могут, скажем, выполнять или не выполнять перед уничтожением объекта какие-либо действия, вроде сохранения данных на диск, вывода того или иного сообщения и т.д.
Методы объектов и оператор with
Конструктор и деструктор вкупе с методом Free — далеко не единственные стандартные методы, достающиеся создаваемым классам в наследство от TObject. Рассмотрим здесь некоторые из них, которые могут быть полезными при работе с объектами в Delphi.
Начнем с того, что для конструктора и деструктора предусмотрены два дополнительных метода, которые вызываются автоматически сразу после создания объекта и непосредственно перед уничтожением. Это AfterConstruction и BeforeDestruction. Хотя к самим этим методам обращаться напрямую нельзя — они вызываются автоматически, допускается определить в них код, который следует выполнить после создания объекта, или перед его удалением.
Если в коде программы требуется выяснить, к какому классу принадлежит тот или иной объект, можно использовать метод ClassName, возвращающий имя класса в виде строки. А если требуется просто проверить, не является ли имя класса у объекта каким-либо конкретным значением, используют метод ClassNameIs:
str := User.ClassName; // в str получим имя класса, например, ‘TUser’ if User.ClassNameIs(‘TUser’) then . // условие будет верно, если объект User является экземпляром класса TUser
Для того же случая, если требуется получить ссылку именно на сам класс, а не его имя, предусмотрен метод ClassType. Однако напрямую его использовать не следует — для этих целей существует оператор is, который применим не только к простым типам, но и к классам:
if User is TUser then . // условие будет верно, если объект User является экземпляром класса TUser
Собственно говоря, другой оператор этой группы — as — так же применим к объектам. Например, если мы хотим использовать переменную User2, являющуюся экземпляром некого класса TCustomUser, в качестве объекта типа TUser, мы можем написать такое выражение:
(User2 as TUser).Login := ‘Иван Иваныч’; (User2 as TUser).Password := ‘пароль’;
Такое выражение будет допустимо, если у того класса, которому принадлежит объект User2 (т.е. TCustomUser), так же имеются свойства Login и Password, причем они должны быть такого же типа, как и у TUser.
Рассмотренные методы иногда бывают полезными при работе с объектами, поскольку позволяют выполнить те или иные операции. А вот оператор with полезен скорее программисту, пишущему программу, поскольку в ряде случаев позволяет существенно сократить размер кода. Например, предыдущие выражения при помощи оператора with можно записать так:
With (User2 as TUser) do begin Login := ‘Иван Иваныч’; Password := ‘пароль’; end;
Фактически, при помощи оператора with мы можем, единожды указав часть выражения до точки, далее его опускать до тех пор, пока не окончится блок with. При этом не важно, сколько уровней вложения будет использовано. Например, если взять класс TUser, который в качестве поля MegaInfo имеет класс TMegaUserInfo, а оно, в свою очередь, имеет поле Address, являющееся записью типа TAddress, то для обращения к полям записи всякий раз придется указывать довольно-таки длинные строки, определяющие расположение того или иного фрагмента данных:
User.MegaInfo.Address.PostIndex := ‘119071’; User.MegaInfo.Address.City := ‘Москва’;
Вместо этого, с использованием оператора with, можно лишь единожды написать весь «путь» к этим данным, в дальнейшем обращаясь лишь к самим полям:
With User.MegaInfo.Address do begin PostIndex := 119071; . end;
В операторе with допустимо использовать сразу несколько таких путей. Например, если надо будет обращаться и к полям записи Address, и непосредственно к полям User, то можно использовать такой синтаксис:
With User, User.MegaInfo.Address do begin PostIndex := 119071; Login := ‘Иван Иваныч’; MegaInfo.Phone := ‘123-4567’; . end;
Здесь в первой строке будет использован второй путь — до полей Address, а во второй и третьей — первый, до полей объекта User. Попутно отметим, что оператор with может использоваться не только с классами, но и с обычными записями.
Классы в действии
Попробуем применить инкапсуляцию, наследование и полиморфизм в одном проекте, заодно воспользовавшись оператором with, и посмотрев, как все это работает. Для этого создадим несколько классов, причем один из них будет наследником другого. В качестве родительского класса используем TCustomUser, а класс TUser сделаем его наследником. Еще один класс — TMegaUserInfo будет являться полем класса TCustomUser. Наконец, одним из полей класса TMegaUserInfo пусть будет запись TAddress. В таком случае порядок объявления типов должен производиться в следующей последовательности: TAddress, TMegaUserInfo, TCustomUser, TUser. Таким образом, объявления типов на начальном этапе могут выглядеть таким образом, как показано в листинге 7.2.
Листинг 7.2. Черновое определение типов
type TAddress = record PostIndex: integer; City: string; end; TMegaUserInfo = class Address: TAddress; Phone: string; end; TCustomUser = class protected fRealName: string; public MegaInfo: TMegaUserInfo; end; TUser = class (TCustomUser) private fLogin: string; fPassword: string; procedure setPassword(newpass: string); published property Login: string read fLogin write fLogin; property Password: string read fPassword write setPassword; end;
Что касается функционального содержимого классов, то для TCustomUser нам придется использовать собственный конструктор, поскольку одним из его полей является другой класс, из которого необходимо создать объект во время выполнения программы. Попутно конструктор может принимать исходное значение для одного из полей этого класса, скажем, для fRealName. А поскольку данное поле мы определили как защищенное, то неплохо было бы предусмотреть еще и общедоступный метод для извлечения этого значения. Это может быть свойство или функция. Выберем функцию и назовем ее GetRealName. Наконец, раз есть конструктор, создающий дополнительные объекты то нужен еще и деструктор, их уничтожающий. В результате для этого класса определение будет таким:
TCustomUser = class protected fRealName: string; public MegaInfo: TMegaUserInfo; function GetRealName: string; constructor Create(username: string); destructor Destroy; override; end;
Теперь займемся дочерним классом TUser. В дополнение к унаследованным от объекта TCustomUser полям и методам, он получит собственные. Пусть это будут свойства Login и Password, вместе с корреспондирующими защищенными переменными. Кроме того, предусмотрим для него конструктор, который будет устанавливать одно из свойств, скажем, Login, в какое-либо значение, определяемое при создании экземпляра класса. В результате мы получим следующее:
TUser = class (TCustomUser) private fLogin: string; fPassword: string; procedure setPassword(newpass: string); published property Login: string read fLogin write fLogin; property Password: string read fPassword write setPassword; constructor Create(username, userlogin: string); end;
Отметим, что по той причине, что конструктор этого класса должен вызывать конструктор родительского, то ему придется принимать сразу 2 параметра — для себя и для «родителя». Зато деструктор в данном случае нам не понадобится, поскольку очистку памяти, занимаемую полем-объектом MegaInfo, выполнит деструктор родительского класса, к которому данное поле и принадлежит. В этом мы убедимся по ходу выполнения программы. Для этого достаточно во всех методах этих классов предусмотреть вывод сообщения, рапортующего о ходе его вызова. Например, метод GetRealName будет выглядеть следующим образом:
function TCustomUser.GetRealName: string; begin result:=fRealName; writeln(‘ >> Calls TCustomUser.GetRealName’); end;
В других методах мы так же разместим подобные инструкции для вывода сообщения об его исполнении и о полученных параметрах (разумеется, при их наличии). В результате заголовочная часть программы получит вид, показанный на листинге 7.3.
Листинг 7.3. Классы в полном составе
program class2; <$APPTYPE CONSOLE>type TAddress = record PostIndex: integer; City: string; end; TMegaUserInfo = class Address: TAddress; Phone: string; end; TCustomUser = class protected fRealName: string; public MegaInfo: TMegaUserInfo; function GetRealName: string; constructor Create(username: string); destructor Destroy; override; end; TUser = class (TCustomUser) private fLogin: string; fPassword: string; procedure setPassword(newpass: string); published property Login: string read fLogin write fLogin; property Password: string read fPassword write setPassword; constructor Create(username, userlogin: string); end; function TCustomUser.GetRealName: string; begin result:=fRealName; writeln(‘ >> Calls TCustomUser.GetRealName’); end; constructor TCustomUser.Create(username: string); begin inherited Create; MegaInfo:=TMegaUserInfo.Create; fRealName:=username; writeln(‘ >> Calls TCustomUser.Create with ‘+username); end; destructor TCustomUser.Destroy; begin MegaInfo.Free; inherited Destroy; writeln(‘ >> Calls TCustomUser.Destroy’); end; constructor TUser.Create(username, userlogin: string); begin inherited Create(username); fLogin:=userlogin; fPassword:=’password’; writeln(‘ >> Calls TUser.Create with ‘+username+’ and ‘+userlogin); end; procedure TUser.setPassword(newpass: string); begin if Length(newpass)>3 then fPassword:=newpass else writeln(‘Error! Password is too short!’); writeln(‘ >> Calls TUser.setPassword with ‘+newpass); end; var User: TUser; begin . end.
В результате, если вызвать такой метод класса TUser, который имеет унаследованное представление (в нашем случае — конструктор), то в ходе выполнения программы будут выданы сообщения, показывающие порядок вызова подпрограмм. Например, при обращении к конструктору класса TUser можно будет отследить вызов конструктора класса TCustomUser:
В результате выполнения этой строки будут выданы 2 сообщения: «Calls TCustomUser.Create with Ivan Ivanovich» и «Calls TUser.Create with Ivan Ivanovich and Vano».
Полный код программы можно найти в каталоге Demo\Part2\Class2. В завершение отмечу, что это, пожалуй, будет последним консольным приложением в Delphi среди примеров этой книги.