Для поддержки ООП в языке Delphi используются классовые типы (или просто «классы»), представляющее собой структуру, содержащую так называемые «элементы класса»: поля, методы и свойства. Экземпляр класса (переменная классового типа) называет объектом. Каждый объект включает в себя все элементы, объявленные в его классе.
Поскольку класс это - тип, то он, естественно, объявляется в разделе type. Для описания нового класса в языке Delphi используется зарезервированное слово class.
Пример 1. Общий синтаксис объявления класса в Delphi:
type <имя_нового_класса>=class (<имя_наследуемого_класса>) private // частные элементы класса protected // защищенные элементы класса public // общедоступные элементы класса published // опубликованные элементы класса end;
Во всем объявлении класса единственным обязательным атрибутом является имя нового класса, которое должно быть уникальным (не должно повторяться) в рамках всей библиотеки классов, которая в Delphi называется Visual Component Library (VCL).
По принятому соглашению имя класса начинается с большой буквы "T". Хотя в языке Delphi регистр букв не учитывается, тем не менее, принято большие (прописные) буквы использовать для обозначения начала слова в идентификаторах, состоящих из нескольких слов.
В соответствии с принципом наследования каждый новый класс наследует реализацию и интерфейс от своего непосредственного предка, имя которого указывается в скобках после зарезервированного слова class. Библиотека VCL устроена так, что все классы имеют оного общего предка типа TObject. Поэтому если даже имя наследуемого класса не указывать, то будет подразумеваться TObject.
Пример 2. Пример двух идентичных объявлений
type TMyClass = class ... end; type TMyClass = class(TObject) ... end;
Рекомендуется явно указывать предка (даже если он является TObject) для читабельности кода. Впрочем, в реальном программировании наследовать новый класс непосредственно от TObject приходится крайне редко.
Директивы private, protected, public и published предназначены для разграничения доступа к элементам класса. Обычно они расположены в порядке строгости.
Запомнить последовательность директив несложно благодаря тому, что они расположены по алфавиту.
Однако, количество директив и порядок их следования может быть, строго говоря, произвольным.
Так, в секции private содержит «внутренние (частные)» элементы класса, обращение к которым возможно только в пределах модуля, содержащего объявление класса. Ни наследники класса, объявленные в другом модуле, ни другие объекты приложения не имеют доступа к элементам секции private. Другими словами, секция private - «частная собственность» класса.
Секция protected содержит «защищенные» элементы, доступ к которым имеют только классы-наследники (потомки). Никакие объекты приложения (в том числе и класса-наследника) не имеют доступа к элементам этой секции.
Секция public содержит «общедоступные» элементы, обращение к которым возможно из любой части программы во время ее выполнения (так называемого Runtime).
Секция published содержит «опубликованные» элементы, обращение к которым возможно не только из любой части программы во время ее выполнения, но и во время проектирования (Designtime).
Строго говоря, существует еще одна директива видимости - automated, предназначенная для объявления атрибутов класса, являющегося сервером автоматизации с использованием технологии COM.
Директива automated является устаревшей, оставлена только для обеспечения обратной совместимости и реальной необходимости использовании этой директивы нет.
Если явно не указать директиву видимости, то атрибут класса по умолчанию будет published если класс компилируется при включенной директиве компилятора {$M+} или класс унаследован от класса, откомпилированного при этой включенной директиве {$M+}. Во всех других случаях по умолчанию видимость - public.
Во избежание недоразумений всегда явно определяйте директивы видимости.
Потомки класса могут повышать доступность элементов, объявленных в секциях protected и public, перемещая их в более доступные секции.
Скрыть же элементы, переместив в более защищенную секцию, потомки не могут. Даже если сделать это, то ошибок компиляции не возникнет, однако эффекта сокрытия не возникнет.
Разработаем для примера объектную модель телефона.
Пример 3. Простейшее объявление некого "телефона вообще" будет выглядеть следующим образом:
TPhone=class end;
Пока от такого «телефона» мало пользы, но по ходу обсуждения мы будем наращивать этот «скелет».
Все объекты Delphi являются динамическими, а переменная классового типа фактически является указателем на объект. Поэтому для работы с экземпляром класса (объектом) необходимо его создать, выделив необходимое количество оперативной памяти. Для этого используются специальные методы класса, называемые «конструкторами».
Обратите внимание на то, что при создании нового объекта в памяти выделяется место только для его полей. Методы, как и обычные процедуры и функции, помещаются в область кода программы. Они работают со всеми экземплярами своего класса и поэтому не дублируются в памяти.
По окончании работы с объектом необходимо удалить его из динамической памяти с помощью специального метода, называемого «деструктором».
В любом классе есть хотя бы один конструктор с именем Create и хотя бы один деструктор Destroy, объявленные в TObject.
Пример 4. Создадим, а затем разрушим объект класса TPhone
var Phone:TPhone; begin Phone:= TPhone.Create; // Код работы с объектом if Assigned(Phone) then Phone.Destroy; // или просто Phone.Free; end;
Класс может иметь и другие конструкторы и деструкторы, отличающиеся параметрами.
Совокупность значений полей объекта определяет его состояние. Изменение их значений отражает изменение состояния моделируемого объекта. Реакция на происходящие события описывает его взаимоотношения с другими объектами.
Поля объекта по сути дела представляют собой переменные, объявленные внутри класса. Они могут быть любого типа, в том числе и классового.
Обычно они размещаются в секции private и используются для внутренних нужд объекта (например, для хранения значений свойств). Все поля связываются статически, то есть ссылки на них формируются на этапе компиляции и не могут быть изменены.
Воздействие на объект осуществляется путем изменения его полей и вызова его методов. При этом названия полей и методов отделяются от имени объекта точкой.
Продолжим пример с телефоном. Допустим, для некоторой интерактивной программы-каталога нам необходимо создать объектную модель телефонной техники, позволяющую продемонстрировать не только внешний вид, но и имитировать работу телефона. Очевидно, что телефонные аппараты (стационарные, мобильные различных стандартов, факсы) обладают схожим, но не одинаковым набором свойств и реализованных в них методов. Поэтому для построения иерархии необходимо объявить общего предка. Назовем его просто TPhone и будем понимать под ним некий абстрактный телефон.
Пример 5. Для простоты примера опишем только несколько методов - принять входящий звонок и ответить на него (снять трубку):
TPhone=class(TObject) procedure Incoming; // входящий звонок procedure Ring; // звонить procedure HandUp; // ответить (снять трубку) end;
Пример 6. Определим классы для стационарного (комнатного) и мобильного телефонов, дублирующие все методы предка:
TRoomPhone=class(TPhone)// комнатный телефон procedure Incoming; // входящий звонок procedure Ring; // звонить procedure HandUp; // ответить (снять трубку) end; TCellPhone=class(TPhone) // мобильный телефон procedure Incoming; // входящий звонок procedure Ring; // звонить procedure HandUp; // ответить (снять трубку) end;
Телефоны реагируют на входящие звонки по-разному: у простого комнатного просто звонит звонок, а мобильный проигрывает определенную мелодию и включает подсветку экрана. Поэтому и реализация методов Ring у них будет отличаться.
Итак, при создании иерархии обнаружилось, что некоторые элементы объектов, сохраняя название, изменяются по сути. Здесь реализуется принцип полиморфизма, заключающийся в том, что объекты разных классов могут реализовать методы по-своему.
В приведенном примере для методов Ring и HandUp компилятор отведет отдельные адреса и вызвать, например, метод Ring класса TPhone из объекта-наследника не будет никакой возможности.
Такой полиморфизм называется простым, а методы, имеющие одинаковые названия и различную реализацию, - статически полиморфными.
Итак, классы TRoomPhone и TCellPhone реализуют свою логику метода Ring, однако в методе Incoming, реализующем реакцию на входящий звонок, есть схожая логика - вызов звонка Ring. Поэтому было бы хорошо определить метод Incoming в классе TPhone так, чтобы он реализовывал бы общую для всех объектов логику без его полного переопределения в наследниках. Однако в методе Incoming ссылки на метод Ring формируются статически на этапе компиляции, и вызвать методы наследников нет возможности.
Выход из такой ситуации заключается в применении сложного полиморфизма с помощью виртуальных методов, адрес которых становиться известен только на этапе выполнения.
Для того чтобы сделать метод виртуальным нужно после его определения в классе добавить зарезервированное слово virtual.
В классах-потомках для переопределения или дополнения (не замены!) функциональности виртуального метода используется зарезервированное слово override.
Пример 7. Изменим описание классов следующим образом:
TPhone=class(TObject) procedure Incoming; // входящий звонок procedure Ring; virtual;// виртуальный метод procedure HandUp; // ответить (снять трубку) end; TRoomPhone=class(TPhone) // комнатный телефон procedure Ring; override; // переопределенный метод procedure HandUp; // ответить (снять трубку) end; TCellPhone=class(TPhone) // мобильный телефон procedure Ring; override; // переопределенный метод procedure HandUp; // ответить (снять трубку) end;
При работе с виртуальными методами следует соблюдать следующие правила:
если в классе-предке метод описан как виртуальный, то все классы-наследники, переопределяющие его, должны описывать этот метод как полиморфный с помощью слова override;
не следует виртуальный метод предка в наследнике заменять статическим;
формальные параметры виртуальных методов должны быть идентичны.
Для реализации сложного полиморфизма кроме виртуальных методов в Delphi используются динамические методы. По возможностям наследования и перекрытия они аналогичны виртуальным, но отличаются несколько меньшим расходом памяти при большом количестве методов и самих классов. Для объявления метода динамическим используется директива dynamic. Перекрытие динамических методов производиться так же, как и виртуальных - с использованием слова override.
В разработанной иерархии есть еще одна сложность. В абстрактном телефоне TPhone хотя и определен метод снятия трубки HandUp, но реализовывать в нем нечего, так как логика его работы будет существенно различаться. Тем не менее, такой метод должен быть обязательно реализован во всех потомках. В таких случаях прибегают к описанию в базовом классе абстрактных виртуальных или динамических методов, реализация которого возлагается на классы-потомки.
Пример 8. Для объявления метода абстрактным используется зарезервированное слово abstract.
TPhone=class(TObject) procedure Incoming; // входящий звонок procedure Ring; virtual; // звонить procedure HandUp; virtual; abstract; // как-то ответить end; TRoomPhone=class(TPhone) // комнатный телефон procedure Ring; override; // звонить procedure HandUp; override; // ответить с комнатного end; TCellPhone=class(TPhone) // мобильный телефон procedure Ring; override; // звонить procedure HandUp; override; // ответить с мобильного end;
Теперь каждый класс реализует метод HandUp по-своему.
На оценку "удовлетворительно":
Объявить простой класс (см. Пример 1);
Добавить в класс один метод (реализация метода остаётся за вами) (см. Раздел 1.2.1.4);
Создать объект (см. Пример 4), вызвать метод, а затем - разрушить объект;
Перекрыть в новом классе виртуальный метод (см. Раздел 1.2.1.4.2);
Создать объект нового класса, а затем - разрушить объект;
На оценку "отлично":