// Лабораторная работа 2. Модули. Выполнил Сергеев Андрей, группа 999.
// Модуль подпрограмм обработки списков.
unit Lab2Un; // заголовок модуля обработки списков
interface // интерфейс модуля
type
tValue=Integer; // тип элемента списка – целый
pItem=^tItem; // тип указателя на элемент списка
tItem=record // тип элемента списка
Value: tValue; // содержательная часть элемента списка
Next: pItem; // указатель на следующий элемент списка
end; // record tItem
// Заголовки процедур работы со списками
procedure Create(var List: pItem); // создание пустого списка
procedure InsertFirst(var List:pItem;v:tValue); // включение в начало
procedure InsertLast(var List:pItem;v:tValue); // включение в конец
procedure WriteList(var f:Text;List:pItem); // вывод списка в файл
function Size(List: pItem): Word; // вычисление размера
procedure Clear(var List: pItem); // удаление элементов
implementation // Секция реализации модуля
// Реализация процедур работы со списками
procedure Create(var List: pItem);
Begin
// Текст в лабораторной работе 1
end; //procedure Create
procedure InsertFirst(var List: pItem; v: tValue);
// Текст в лабораторной работе 1
end; // procedure InsertFirst
procedure InsertLast(var List: pItem; v: tValue);
// Текст в лабораторной работе 1
end; // procedure InsertLast
procedure WriteList(var f: Text; List: pItem);
// Текст в лабораторной работе 1
end; // procedure WriteList
function Size(List: pItem): Word;
// Текст в лабораторной работе 1
end; // function Size
procedure Clear(var List: pItem);
// Текст в лабораторной работе 1
end; // procedure Clear
end.
// Лабораторная работа 2. Модули.
// Использование модуля подпрограмм обработки обработки списков.
// Выполнил Сергеев Андрей, группа 999.
// Исходные данные – элементы основного списка – в файле LW4Dat.txt
// Результаты работы помещаются в файл LW2Res.txt
program LW2;
uses
SysUtils,
LW2Un in 'LW2Un.pas'; // использование модуля обработки списков
procedure FormLists(L:pItem;var L1,L2:pItem); //формирование L1, L2
// Текст в лаб.раб. 1
end; // procedure FormLists
|
var
fDat, fRes:Text; // файлы с исходными данными и результатами
L, L1, L2:pItem; // указатели на исходный и полученные списки
v:tValue; // значение элемента списка
begin
Assign(fDat,'LW2Dat.txt'); Reset(fDat); // открытие файла для чтения
Assign(fRes,'LW2Res.txt'); Rewrite(fRes); // открытие файла для записи
Create(L); Create(L1); Create(L2); // создание пустых списков
while not eof(fDat) do begin // пока не достигнут конец файла fDat
Read(fDat, v); // чтение из файла очередного значения
InsertFirst(L, v); // вставка элемента со значением v в начало списка L
end; // while
// Вывод списка L и его размера:
Writeln(fRes, 'Исходный список:'); WriteList(fRes, L);
Writeln(fRes, 'Число элементов списка: ', Size(L));
FormLists(L, L1, L2); // формирование списков L1 и L2
Writeln(fRes, 'Список отрицательных нечетных элементов:'); WriteList(fRes, L1);
Writeln(fRes, 'Список положительных четных элементов:'); WriteList(fRes, L2);
Clear(L); Clear(L1); Clear(L2); // удаление списков
Close(fDat); Close(fRes); // закрытие файлов
end.
Тема 3. ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ
1. Основные концепции ООП
Язык Delphi реализует концепцию объектно-ориентированного программирования. Это означает, что функциональность приложения определяется набором взаимосвязанных задач, каждая из которых становится самостоятельным объектом. У объекта есть свойства (т.е. характеристики, или атрибуты) и методы, определяющие его поведение. В основе ООП лежит понятие класса. Класс представляет собой дальнейшее развитие концепции типа и объединяет в себе задание не только структуры и размера переменных, но и выполняемых над ними операций. Объекты в программе всегда являются экземплярами того или иного класса (подобно переменным определенного типа).
К основным концепциям ООП относятся следующие: инкапсуляция, наследование, полиморфизм.
|
Ø Инкапсуляция представляет собой объединение данных и обрабатывающих их методов (подпрограмм) внутри класса (объекта). Это означает, что в классе инкапсулируются (объединяются и помещаются внутрь) поля, свойства и методы. При этом класс получает определенную функциональность, например, обеспечивая полный набор средств для работы с какой-либо динамической структурой данных.
Ø Наследование – это процесс порождения новых объектов-потомков от существующих объектов-родителей, при этом потомок наследует от родителя все его поля, свойства и методы. Просто наследование большого смысла не имеет, поэтому в объект-потомок добавляются новые элементы, определяющие его особенность и функциональность. Удалить какие-либо элементы родителя в потомке нельзя. В свою очередь, от нового объекта можно породить следующий объект, в результате образуется дерево объектов (называемое также иерархией классов).
В корне этого дерева находится базовый класс tObject, который реализует наиболее общие для всех классов элементы, например, действия по созданию и удалению объекта. Чем дальше тот или иной класс отстоит в дереве от базового класса, тем большей специфичностью он обладает.
Пример объявления класса-потомка:
tAnyClass = class (tParentClass)
// Добавление к классу tParentClass новых
// и переопределение существующих элементов
end;
Ø Сущность полиморфизма заключается в том, что методы различных классов могут иметь одинаковые имена, но различное содержание. Это достигается переопределением родительского метода в классе-потомке. В результате родитель и потомок в одинаковых ситуациях ведут себя по-разному.
|
Для изменения метода необходимо перекрыть его в потомке, т.е. объявить в потомке одноименный метод и реализовать в нём нужные действия. В результате в объекте-родителе и объекте-потомке будут действовать два одноимённых метода (возможно с разным набором параметров), имеющие разную реализацию и, следовательно, придающие объектам разные свойства. Это и называется полиморфизмом объектов.
Полиморфизм достигается не только механизмом наследования и перекрытия методов родителя, но и их виртуализацией (см. ниже), позволяющей родительским методам обращаться к методам своих потомков.
2. Классы и объекты
Классом в Delphi называется особая структура, состоящая из полей, методов и свойств. Такой тип также будем называть объектным типом.
Type
tMyClass = class (tObject) // класс tMyClass
fMyField: Integer; // поле fMyField
function MyMethod(Data:tData): Integer; // метод MyMethod
end;
Поскольку класс – это тип, то принято перед собственно именем записывать префикс t (от слова type – тип). Переменная типа класс называется экземпляром класса, или объектом:
var AMyObject: tMyClass; // объект класса tMyClass
Поля класса – это данные, уникальные для каждого созданного в программе экземпляра класса. Они могут иметь любой тип, в том числе – тип класс. В Delphi перед именами полей принято ставить символ f (от field – поле): fLength, fWidth, fMyFileld и т. п.
Методы – это процедуры и функции, описанные своими заголовками внутри класса и предназначенные для операций над его полями. В отличие от полей, методы у всех объектов одного класса общие. От обычных процедур и функций методы отличаются тем, что им при вызове передается (неявно) указатель на тот объект, который их вызвал. Поэтому обрабатываться будут поля именно того объекта, который вызвал метод. Внутри метода указатель на вызвавший его объект доступен под зарезервированным именем Self.
Свойство предварительно определим как поле, доступное для чтения и записи не напрямую, а через соответствующие методы.
Классы могут быть описаны либо в разделе описания типов в самой внешней части программы, либо в секции интерфейса модуля, либо на верхнем уровне вложенности секции реализации. Не допускается описание классов внутри процедур и других блоков кода.
Методы класса реализуются в разделе описания процедур и функций программы или разделе implementation модуля. При реализации метода указывается его полное имя, состоящее из имени класса, точки и имени метода класса:
function tMyClass.MyMethod(Data:tData): Integer; // заголовок метода tMyClass. MyMethod
Begin
… // тело метода tMyClass.MyMethod
end;
Разрешено опережающее объявление классов единственным словом class с последующим полным описанием:
Type
tFirstClass = class;
tSecondClass = class (tObject)
f1: tFirstClass;
end;
tFirstClass = class (tObject)
f2: tSecondClass;
end;
3. Создание и уничтожение объектов
В Delphi объекты могут быть только динамическими! Любая переменная объектного типа есть указатель, причем для доступа к данным, на которые ссылается указатель объекта не нужно применять символ ^.
Для выделения памяти экземпляру любой динамической переменной используется процедура New, для уничтожения – процедура Dispose. С объектами эти процедуры не используются: объект создается специальным методом – конструктором, а уничтожается – деструктором:
AMyObject:= tMyClass.Create; // создание объекта AMyObject класса tMyClass
… // действия с созданным объектом
AMyObject.Destroy; // уничтожение объекта AMyObject
Конструктор – это специальный метод, заголовок которого начинается зарезервированным словом constructor. Функция конструктора заключается в выделении памяти под экземпляр класса (объект) и установлении связи между созданным объектом и специальной информацией о классе. В теле конструктора можно расположить любые операторы, которые необходимо выполнить при создании объекта, например, присвоить полям начальные значения. В Delphi конструкторов у класса может быть несколько. Общепринято называть конструктор Create.
До передачи управления телу конструктора происходит собственно создание объекта – под него отводится память, значения всех полей обнуляются. Далее выполняется код конструктора, написанный программистом. Фактически конструктор – это функция, возвращающая созданный и проинициализированный объект.
Конструктор создает новый экземпляр объекта только в том случае, если перед его именем указано имя класса. Если указать имя уже созданного объекта, он поведет себя по-другому: не создаст новый экземпляр, а только выполнит код, содержащийся в теле конструктора.
Конструктор класса-потомка практически всегда должен перекрывать конструктор класса-предка. Чтобы правильно проинициализировать в создаваемом объекте поля, относящиеся к классу-предку, нужно в начале кода конструктора вызвать конструктор предка с помощью зарезервированного слова inherited:
constructor tMyClass.Create;
Begin
inherited Create;
...
end;
Деструктор – это специальный виртуальный (см. ниже) метод, заголовок которого начинается зарезервированным словом destructor. Деструктор предназначен для удаления объектов. Типичное название деструктора – Destroy. Пример описания класса с конструктором и деструктором:
Type
tMyClass = class (tObject) // класс tMyClass
f MyField: Integer; // поле f MyField
constructor Create; // конструктор Create
destructor Destroy; override; // деструктор Destroy
function MyMethod(Data:tData): Integer; // метод MyMethod
end;
Для уничтожения объекта рекомендуется использовать метод Free (он есть у базового класса tObject), который первоначально проверяет указатель на объект (не равен ли он nil) и только затем вызывает деструктор Destroy:
AMyObject.Free;
Деструктор очень часто не переопределяется в классе-потомке.
4. Инкапсуляция. Свойства
Классическое правило объектно-ориентированного программирования утверждает, что для обеспечения надежности нежелателен прямой доступ к полям объекта: чтение и обновление их содержимого должно производиться посредством вызова соответствующих методов. Это правило и называется инкапсуляцией. В Delphi пользователь объекта может быть полностью отгорожен от полей при помощи свойств.
Обычно свойство определяется тремя своими элементами: полем и двумя методами, которые осуществляют его чтение/запись:
Type
tMyClass = class (tObject)
function GetAProperty: tSomeType;
procedure SetAProperty(ANewValue: tSomeType);
property AProperty: tSomeType read GetAProperty write SetAProperty;
end;
В контексте свойства слова read и write являются зарезервированными. Для доступа к значению свойства AProperty явно достаточно написать
AMyObject.AProperty:= AValue;
AVariable:= AMyObject.AProperty;
и компилятор оттранслирует эти операторы в вызовы методов. То есть внешне свойство выглядит в точности как обычное поле.
В методах, входящих в состав свойств, может осуществляться проверка устанавливаемой величины на попадание в допустимый диапазон значении, и вызов других процедур, зависящих от вносимых изменений. Если же потребности в специальных процедурах чтения и/или записи нет, возможно вместо имен методов применять имена полей. Рассмотрим следующую конструкцию:
tPropClass = class (tObject)
fValue: tSomeType;
procedure DoSomething;
function Correct(AValue: Integer):Boolean;
procedure SetValue(NewValue: Integer);
property AValue: Integer read fValue write SetValue;
end;
procedure tPropClass.SetValue(NewValue: Integer);
Begin
if (NewValue<>fValue) and Correct (NewValue) then fValue:= NewValue;
DoSomething;
end;
В этом примере чтение значения свойства AValue означает просто чтение поля fValue. Зато при присвоении ему значения внутри SetValue вызывается сразу два метода.
Если свойство должно только читаться или только записываться, в его описании может присутствовать только соответствующий метод:
tMyClass = class (tObject)
property AProperty: tSomeType read GetValue;
end;
В этом примере вне объекта значение свойства можно лишь прочитать; попытки присвоить AProperty значение вызовет ошибку компиляции.
Для присвоения свойству значения по умолчанию используется ключевое слово default:
property Visible: Boolean read fVisible write SetVisible default True
Свойство может быть и векторным; в этом случае оно внешне выглядит как массив:
property APoints[Index: Integer]:tPoint read GetPoint write SetPoint;
На самом деле, среди полей класса может и не быть поля-массива.
При помощи свойств вся обработка обращений к внутренним структурам класса может быть замаскирована.
Для векторного свойства необходимо описать не только тип элементов массива, но также имя и тип индекса. После ключевых слов read и write в этом случае должны стоять имена методов – использование здесь имени поля типа массив недопустимо. Метод чтения значения векторного свойства должен быть описан как функция, возвращающая значение того же типа, что и элементы свойства, и имеющая единственный параметр: того же типа и с тем же именем, что и индекс свойства:
function GetPoint(Index: Integer): TPoint;
Метод записи значения в такое свойство должен первым параметром иметь индекс, а вторым – переменную нужного типа (которая может быть передана как по ссылке, так и по значению):
procedure SetPoint(Index: Integer; NewPoint: tPoint);
У векторных свойств есть еще одна важная особенность. Некоторые классы в Delphi (списки, наборы строк) «построены» вокруг векторного свойства. Основной метод такого класса дает доступ к некоторому массиву, а все остальные методы являются вспомогательными. Специально для облегчения работы в этом случае векторное свойство может быть описано как default:
tМуClass = class
property Strings[Index: Integer]: string read Get write Put; default;
end;
Когда у объекта есть такое свойство (его называют векторным свойством по умолчанию), то можно его не упоминать, а ставить индекс в квадратных скобках прямо у имени объекта:
var AMyObject: tMyClass;
Begin
AMyObject.Strings[1]:= 'First'; // первый способ
AMyObject[2]:= 'Second'; // второй способ
...
end;
Будьте внимательны, применяя зарезервированное слово default, – для обычных и векторных свойств оно употребляется в разных случаях и с различным синтаксисом.
О роли свойств в Delphi красноречиво говорит следующий факт: у всех имеющихся в вашем распоряжении стандартных классов 100% полей недоступны (помещены в секцию private) и заменены базирующимися на них свойствами. Рекомендуем при разработке своих классов придерживаться этого же правила.
5. Наследование. Методы
Принцип наследования позволяет объявить класс
tNewObject = class (tOldObject);
являющийся потомком или дочерним классом старого класса, называемого предком или родительским классом, и добавить к нему новые поля, методы и свойства.
В Delphi все классы являются потомками класса tObject. При построении дочернего класс прямо от tObject в определении его можно не упоминать. Следующие объявления одинаково верны:
tMyObject = class (tObject);
tMyObject = class;
Первый вариант предпочтительнее, хотя он и более длинный, – для устранения возможных неоднозначностей. Класс tObject несет очень серьезную нагрузку и будет рассмотрен отдельно.
Унаследованные от предка поля и методы доступны в дочернем классе; если имеет место совпадение имен методов, то говорят, что они перекрываются.
По тому, какие действия происходят при вызове, методы делятся на три группы: 1) статические, 2) виртуальные (virtual) и динамические (dynamic), 3) перегружаемые(overload) методы.
Статические методы, а также любые поля в объектах-потомках ведут себя одинаково: можно без ограничений перекрывать старые имена, и при этом изменять тип методов. Перекрытое поле предка недоступно в потомке. Перекрытый метод доступен при указании зарезервированного слова inherited. Методы объектов по умолчанию являются статическими – их адрес определяется еще на стадии компиляции проекта. Они вызываются быстрее всего.
Принципиально отличаются от статических виртуальные и динамические методы (директива virtual или dynamic). Их адрес определяется во время выполнения программы по специальной таблице. С точки зрения наследования методы этих двух видов одинаковы: они могут быть перекрыты в дочернем классе только одноименными методами, имеющимитот же тип.
В Delphi понятие множественного наследования отсутствует. Если вы хотите, чтобы новый класс объединял свойства нескольких, породите классы-предки один от другого или включите в класс несколько полей, соответствующих этим желаемым классам.
6. Полиморфизм. Виртуальные и динамические методы
Рассмотрим пример. Пусть у нас имеются некое обобщенное поле для хранения данных – абстрактный класс tField и три его потомка – для хранения строк, целых и вещественных чисел:
tField = class function GetData: string; virtual; abstract; end; tStringField = class(tField) fData: string; function GetData: string; override; end; tIntegerField = class(tField) fData: Integer; function GetData: string; override; end; tExtendedField = class(tField) fData: Extended; function GetData: string; override; end; | function tStringField.GetData; begin Result:= fData; end; function tIntegerField.GetData; begin Result:= IntToStr(fData); end; function tExtendedField.GetData; begin Result:=FloatToStr(fData); end; procedure ShowData(AField:tField); begin Writeln(AField.GetData); end; |
В этом примере классы содержат разнотипные данные и «умеют» сообщать о значении этих данных текстовой строкой (при помощи метода GetData). Внешняя по отношению к ним процедура ShowData получает объект в виде параметра и показывает эту строку.
Правила контроля соответствия типов (typecasting) языка Delphi гласят, что объекту как указателю на экземпляр объектного типа может быть присвоен адрес любого экземпляра любого из дочерних типов. В процедуре ShowData параметр описан как tField – это значит, что в нее можно передавать объекты классов и tStringField, и tIntegerField, и tExtendedField, и любого другого потомка tField.
Но чей метод GetData при этом будет вызван? Тот, который соответствует классу фактически переданного объекта. Этот принцип называется полиморфизмом и он, пожалуй, представляет собой наиболее важный принцип ООП. Например, чтобы смоделировать некоторую совокупность явлений или процессов средствами ООП, нужно выделить их самые общие, типовые черты. Те из них, которые не изменяют своего содержания, должны быть реализованы в виде статических методов. Те же, которые варьируются при переходе от общего к частному, лучше облечь в форму виртуальных методов. Основные, «родовые» черты (методы) нужно описать в классе-предке и затем перекрывать их в классах-потомках.
При вызове виртуальных и динамических методов адрес определяется не во время компиляции, а во время выполнения – это называется поздним связыванием (late binding). Позднее связывание реализуется с помощью таблицы виртуальных методов (Virtual Method Table, VMT) и таблицы динамических методов (Dynamic Method Table, DMT).
Разница между виртуальными и динамическими методами заключается в особенности поиска адреса. Когда компилятор встречает обращение к виртуальному методу, он подставляет вместо обращения к конкретному адресу код, который обращается к VMT этого объекта и извлекает оттуда нужный адрес. Такая таблица одна для каждого класса (объектного типа). В ней хранятся адреса всех виртуальных методов класса, независимо от того, унаследованы ли они от предка или перекрыты. Отсюда и достоинства, и недостатки виртуальных методов: они вызываются сравнительно быстро (но медленнее статических), однако для хранения указателей на них требуется большое количество памяти.
Динамические методы вызываются медленнее, но позволяют более экономно расходовать память. Каждому динамическому методу системой присваивается уникальный индекс. В таблице динамических методов класса хранятся индексы и адреса только тех динамических методов, которые описаны в данном классе. При вызове динамического метода происходит поиск в этой таблице; в случае неудачи просматриваются таблицы DMT всех классов-предков в порядке иерархии и, наконец, DMT класса tObject, где имеется стандартный обработчик вызова динамических методов. Экономия памяти налицо.
Для перекрытия и виртуальных, и динамических методов служит директива override, с помощью которой (и только с ней!) можно переопределять оба этих типа методов. Приведем пример:
tClass1 = class fField1: Integer; fField2: Longint; procedure stMet; procedure vrMet1; virtual; procedure vrMet2; virtual; procedure dnMet1; dynamic; procedure dnMet2; dynamic; end; | tClass2 = class(tClass1) procedure stMet; procedure vrMet1; override; procedure dnMet1; override; end; var Obj1: tClass1; Obj2: tClass2; |
Первый метод класса tClass2 создается заново, остальные два – перекрываются. Попытка применить директиву override к статическому методу вызовет ошибку компиляции.
7. Пример реализации полиморфизма для иерархии графических объектов
Рассмотрим описание и реализацию классов «Точка» и «Окружность» для «рисования» на экране точки и окружности.
Type
tCoord = Word; // тип – координата точки на экране
// Описание класса tPoint
tPoint = class (tObject) // класс – точка на экране дисплея
Protected
fX, fY: tCoord; // поля – координаты точки на экране
fColor: Byte; // поле – цвет рисования точки
Public
property X: tCoord read fX write fX; // свойство – координата X
property Y: tCoord read fY write fY; // свойство – координата Y
property Color: Byte read fColor write fColor; // свойство – цвет
procedure Show; // метод высвечивания точки
procedure Hide; // метод гашения точки
procedure MoveTo(NewX, NewY: tCoord); // метод перемещения точки
end;
// Описание класса tCircle
tCircle = class (tPoint) // класс – окружность на экране
Protected
fRadius: tCoord; // поле – радиус окружности
Public
property Radius: tCoord read fRadius write fRadius; // свойство – радиус
procedure Show; // метод высвечивания окружности
procedure Hide; // метод гашения окружности
procedure MoveTo (NewX,NewY:tCoord) // метод перемещения окружности
end;
// Реализация методов класса tPoint
procedure tPoint.Show;
Begin
Writeln('Рисую точку (', fx, ', ', fy, ') цветом ', fColor); // «рисование» точки
end;
procedure tPoint.Hide;
Begin
Writeln('Гашу точку (', fx, ', ',fy, ') цвета ', fColor); // «гашение» точки
end;
procedure tPoint.MoveTo(NewX, NewY: tCoord);
Begin
Hide;
fX:= NewX; fY:= NewY;
Show;
end;
// Реализация методов класса tCircle
procedure tCircle.Show;
begin // «рисование» окружности
Writeln('Рисую окружность с центром (',fx,',',fy,') радиуса ',fRadius,' цветом ',fColor);
end;
procedure tCircle.Hide; // «гашение» окружности
Begin
Writeln('Гашу окружность с центром (', fx, ', ', fy, ') радиуса ', fRadius, ' цвета ', fColor);
end;
procedure tCircle.MoveTo (NewX,NewY: tCoord);
Begin
Hide;
fX:= NewX; fY:= NewY;
Show;
end;
Обратите внимание, что методы MoveTo классов tPoint и tCircle реализуются одинаково за исключением того, что в tPoint.MoveTo используются tPoint.Hide и tPoint.Show, а в tCircle.MoveTo используются tCircle.Hide и tCircle.Show. Взаимодействие методов классов tPoint и tCircle можно проиллюстрировать следующей схемой:
Поскольку методы реализуются одинаково, можно попытаться унаследовать метод MoveTo от класса tPoint. При этом возникает следующая ситуация. При компиляции метода tPoint.MoveTo в него будут включены ссылки на коды методов tPoint.Show и tPoint.Hide. Так как метод с именем MoveTo не определен в классе tCircle, то компилятор обращается к родительскому типу и ищет в нем метод с этим именем. Если метод найден, то адрес родительского метода заменяет имя в исходном коде потомка. Следовательно, при вызове tCircle.MoveTo в программу будут включены коды tPoint.MoveTo (то есть класс tCircle будет использовать метод так, как он откомпилирован). В этом случае процедура tCircle.MoveTo будет работать неверно.
Структура вызовов методов при этом будет следующей:
В нашем примере процедуры Show и Hide должны быть объявлены виртуальными, так как только в этом случае класс tCircle может наследовать метод MoveTo у типа tPoint. Это становится возможным потому, что подключение виртуальных методов Show и Hide к процедуре MoveTo будет осуществляться во время выполнения программы, и будут подключаться методы, определенные для типа tCircle (tCircle.Show и tCircle.Hide). Напомним, что подключение виртуальных методов осуществляется с помощью специальной таблицы виртуальных методов (VMT).
При использовании виртуальных методов Show и Hide взаимодействие методов классов tPoint и tCircle можно проиллюстрировать следующей схемой:
Описание классов «Точка» и «Окружность» с использованием виртуальных методов:
// Описание класса tPoint
tPoint = class (TObject)
Protected
fX, fY: tCoord;
fColor: Byte;
Public
property X: tCoord read fX write fX;
property Y: tCoord read fY write fY;
property Color: Byte read fColor write fColor;
procedure Show; virtual;
procedure Hide; virtual;
procedure MoveTo(NewX, NewY: tCoord);
end;
// Описание класса tCircle
tCircle = class (tPoint)
Protected
fRadius: tCoord;
Public
property Radius: tCoord read fRadius write fRadius;
procedure Show; override;
procedure Hide; override;
end;
8. Перегрузка методов
В последних версиях Delphi появилась новая разновидность методов – перегружаемые методы. Эту категорию методов нельзя назвать антагонистом двух предыдущих: и статические и динамические методы могут быть перегружаемыми. Перегрузка нужна, чтобы выполнить одинаковые или похожие действия с разнотипными данными. Рассмотрим пример:
Type
tClass1 = class
i: Extended;
procedure SetData(AValue: Extended);
end;
tClass2 = class (tClass1)
j: Integer;
procedure SetData(AValue: Integer);
end;
Var
Obj1: tClass1;
Obj2: tClass2;
Попытка вызова методов
Obj2.SetData(1.0);
Obj2.SetData(1);
вызовет ошибку компиляции на первой из двух строк. Для компилятора внутри Obj2 статический метод с параметром типа Extended перекрыт, и он его не «признает». Объявить методы виртуальными нельзя, так как тип и количество параметров в одноименных виртуальных методах должны совпадать. Чтобы указанные вызовы были верными, необходимо объявить методы перегружаемыми, для чего используется директива overload:
Type
tClass1 = class
i: Extended;
procedure SetData(AValue: Extended); overload;
end;
tClass2 = class (tClass1)
j: Integer;
procedure SetData(AValue: Integer); overload;
end;
Объявив метод SetData перегружаемым, в программе можно использовать обе его реализации одновременно. Это возможно потому, что компилятор определяет тип передаваемого параметра (целый или с плавающей точкой) и в зависимости от этого подставляет вызов соответствующего метода.
Можно перегружать и виртуальные методы. В этом случае необходимо добавить директиву reintroduce:
Type
tClass1 = class
i: Extended;
procedure SetData(AValue: Extended); overload; virtual;
end;
tClass2 = class (tClass1)
j: Integer;
procedure SetData(AValue: Integer); reintroduce; overload;
end;
На перегрузку методов накладывается ограничение – нельзя перегружать методы, находящиеся в области видимости published. Области видимости обсуждаются далее.
9. Абстрактные методы
Абстрактными называются методы, которые определены в классе, но не содержат никаких действий, никогда не вызываются и обязательно должны быть переопределены в потомках класса. Абстрактными могут быть только виртуальные и динамические методы. В Delphi есть одноименная директива (abstract), указываемая при описании метода:
procedure NeverCallMe; virtual; abstract;
Никакого кода для этого метода писать не нужно. Вызов абстрактного метода приведет к созданию исключительной ситуации EAbstractError.
Пример с классом tField из раздела 6 «Полиморфизм» поясняет, для чего нужно использование абстрактных методов. В данном случае класс tField не используется сам по себе; его основное предназначение – быть родоначальником иерархии конкретных классов-«полей» и дать возможность абстрагироваться от частностей. Хотя параметр процедуры ShowData и описан как tField, но если передать в нее объект этого класса, произойдет исключительная ситуация вызова абстрактного метода.
10. Области видимости
Области видимости – это возможности доступа к составным частям объекта. В Delphi поля и методы могут относиться к четырем группам: «общие» (public), «личные» (private), «защищенные» (protected) и «опубликованные» (published).
1. Поля, свойства и методы, находящиеся в секции public, не имеют ограничений на видимость. Они доступны из других функций и методов объектов, как в данном модуле, так и во всех прочих, ссылающихся на него.
2. Поля, свойства и методы, находящиеся в секции private, доступны только в методах класса и в функциях, содержащихся в том же модуле, что и описываемый класс. Такая директива позволяет скрыть детали внутренней реализации класса от всех. Элементы из секции private можно изменять, и это не будет сказываться на программах, работающих с объектами этого класса. Обратиться к ним можно, только переписав содержащий их модуль.
3. Поля, свойства и методы, находящиеся в секции protected, доступны только внутри классов, являющихся потомками данного, в том числе и в других модулях. Такие элементы особенно необходимы дня разработчиков новых компонентов – потомков уже существующих.
4. Область видимости, определяемая четвертой директивой – published, имеет особое значение для интерфейса визуального проектирования Delphi. В этой секции должны быть собраны те свойства объекта, которые будут видны не только во время исполнения приложения, но и из среды разработки. Все свойства компонентов, доступные через Инспектор объектов, являются их опубликованными свойствами. Во время выполнения такие свойства общедоступны, как и public.
Пример, иллюстрирующий первые три варианта областей видимости:
unit First; interface type tClass1 = class public procedure Method1; private procedure Method2; protected procedure Method3; end; procedure TestProc1; implementation var Obj1: tClass1; procedure TestProc1; begin Obj1 := tClass1.Create; Obj1.Method1; // допустимо Obj1.Method2; // допустимо Obj1.Method3; // недопустимо Obj1.Free; end; end. | unit Second; interface uses First; type tClass2 = class(tClass1) procedure Method4; end; procedure TestProc2; implementation var Obj2: tClass2; procedure tClass2.Method4; begin Method1; // допустимо Method2; // недопустимо Method3; // допустимо end; procedure TestProc2; begin Obj2: =tClass2.Create; Obj2.Method1; // допустимо Obj2.Method2; // недопустимо Obj2.Method3; // недопустимо Obj2.Free; end; end. |
При описании дочернего класса можно переносить методы и свойства из одной сферы видимости в другую, не переписывая их заново и даже не описывая – достаточно указать новую сферу видимости наследуемого метода или свойства в описании дочернего класса. Разумеется, если вы поместили свойство в область private, «достать» его оттуда в потомках возможности уже нет.
11. Объект изнутри
Рассмотрим пример из раздела «Полиморфизм»:
tClass1 = class
fField1: Integer;
fField2: Longint;
procedure stMet;
procedure vrMet1; virtual;
procedure vrMet2; virtual;
procedure dnMet1; dynamic;
procedure dnMet2; dynamic;
end;
tClass2 = class (tClass1)
procedure stMet;
procedure vrMet1; override;
procedure dnMet1; override;
end;
Var
Obj1: tClass1;
Obj2: tClass2;
Внутренняя структура объектов Obj1 и Obj2 имеет вид:
Указатель на объект Obj1 | VMT класса tClass1 | DMT класса tClass1 | ||
Указатель на класс tClass1 | RTTI класса tClass1 | Число динамических методов: 2 | ||
Поле fField1 | @tClass1.vrMet1 | Индекс tClass1.dnMet1 (-1) | ||
Поле fField2 | @tClass1.vrMet2 | Индекс tClass1.dnMet2 (-2) | ||
@tObject.Destroy | @tClass1.dnMet1 | |||
@tClass1.dnMet2 | ||||
Указатель на объект Obj2 | VMT класса tClass2 | DMT класса tClass2 | ||
Указатель на класс tClass2 | RTTI класса tClass2 | Число динамических методов: 1 | ||
Поле fField1 | @tClass2.vrMet1 | Индекс tClass2.dnMet1 (-1) | ||
Поле fField2 | @tClass1.vrMet2 | @tClass2.dnMet1 | ||
@tObject.Destroy |
Первое поле каждого экземпляра объекта содержит указатель на его класс. Класс как структура состоит из двух частей. Начиная с адреса, на который ссылается указатель на класс, располагается таблица виртуальных методов, содержащая адреса всех виртуальных методов класса, включая унаследованные от предков. Перед таблицей виртуальных методов расположена специальная структура, которая называется информацией о типе времени выполнения (runtime type information, RTTI). В ней содержатся данные, полностью характеризующие класс: его имя, размер экземпляра, указатели на класс-предок, на имя класса и т. д. На рисунке она показана одним блоком.