ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ В DELPHI
Язык Ое1рЫ реализует концепцию объектно-ориентированного программирования. Это означает, что функциональность приложения определяется набором взаимосвязанных задач, каждая из которых становится самостоятельным объектом. У объекта есть свойства (т.е. характеристики, или атрибуты) и методы, определяющие его поведение. В основе объектно-ориентированного программирования (ООП) лежит понятие класса.
Класс представляет собой дальнейшее развитие концепции типа и объединяет в себе задание не только структуры и размера переменных, но и выполняемых над ними операций.
Объекты в программе всегда являются экземплярами того или иного класса (подобно переменным определенного типа).
К основным концепциям ООП относятся следующие: инкапсуляция, наследование, полиморфизм.
Инкапсуляция представляет собой объединение данных и обрабатывающих их методов (подпрограмм) внутри класса (объекта). Это означает, что в классе инкапсулируются (объединяются и помещаются внутрь) поля, свойства и методы. При этом класс получает определенную функциональность, например, обеспечивая полный набор средств для работы с какой-либо динамической структурой данных.
Наследование — это процесс порождения новых объектов-потом-ков от существующих объектов — родителей, при этом потомок наследует от родителя все его поля, свойства и методы. Просто наследование большого смысла не имеет, поэтому в объект-потомок добавляются новые элементы, определяющие его особенность и функциональность. Удалить какие-либо элементы родителя в потомке нельзя. В свою очередь, от нового объекта можно породить следующий объект, в результате образуется дерево объектов (называемое также иерархией классов).
В корне этого дерева находится базовый класс ЮЬдесЧ который реализует наиболее общие для всех классов элементы, например, действия по созданию и удалению объекта. Чем дальше тот или иной класс отстоит в дереве от базового класса, тем большей специфичностью он обладает.
Пример объявления класса-потомка:
//Добавление к классу tParentClass новых
// и переопределение существующих элементов
Сущность полиморфизма заключается в том, что методы различных классов могут иметь одинаковы имена, но различное содержание. Это достигается переопределением родительского метода в классе-потомке. В результате родитель и потомок в одинаковых ситуациях ведут себя по-разному.
Для изменения метода необходимо перекрыть его в потомке, т.е. объявить в потомке одноименный метод и реализовать в нем нужные действия. В результате в объекте-родителе и объекте-потомке будут действовать два одноименных метода (возможно с разным набором параметров), имеющие разную реализацию и, следовательно, придающие объектам разные свойства. Это и называется полиморфизмом объектов.
Полиморфизм достигается не только механизмом наследования и перекрытия методов родителя, но и их виртуализацией (см. ниже), позволяющей родительским методам обращаться к методам своих потомков.
КЛАССЫ И ОБЪЕКТЫ
Классом в Delphi называется особая структура, состоящая из полей, методов и свойств.
tMyClass = class (TObject) //класс tMyClass
fMyField: Integer; //поле fMyField
function MyMethod(Data:tData): Integer; //метод MyMethod
Все классы в Delphi являются наследниками базового класс TObject, который реализует наиболее общие для всех классов элементы, например, действия по созданию и удалению объекта.
Поля класса — это данные, уникальные для каждого созданного в программе экземпляра класса. Они могут иметь любой тип, в том числе — тип класс. В Delphi перед именами полей принято ставить символ f (от field — поле): fLength, fWidth, fMyFileld и т.п.
Методы — это процедуры и функции, описанные своими заголовками внутри класса и предназначенные для операций над его полями. В отличие от полей, методы у всех объектов одного класса общие. От обычных процедур и функций методы отличаются тем, что им при вызове передается (неявно) указатель на тот объект, который их вызвал. Поэтому обрабатываться будут поля именно того объекта, который вызвал метод. Внутри метода указатель на вызвавший его объект доступен под зарезервированным именем Self.
Свойство можно определить как поле, доступное для чтения и записи не напрямую, а через соответствующие методы.
Классы могут быть описаны либо в разделе описания типов в самой внешней части программы, либо в секции интерфейса модуля, либо на верхнем уровне вложенности секции реализации. Не допускается описание классов внутри процедур и других блоков кода.
Методы класса реализуются в разделе описания процедур и функций программы или в разделе implementation модуля. При реализации метода указывается его полное имя, состоящее из имени класса, точки и имени метода класса:
function tMyClass.MyMethod(Data:tData): Integer;
//заголовок метода tMyClass.МуMethod
// тело метода tMyClass.MyMethod
Разрешено опережающее объявление классов единственным словом class с последующим полным описанием:
tSecondClass = class f1 : tFirstClass;
tFirstClass = class f2 : tSecondClass;
Области видимости — это возможности доступа к составным частям объекта. В Delphi поля и методы могут относиться к четырем группам: «общие» (public), «личные» (private), «защищенные» (protected) и «опубликованные» (published).
- 1. Поля, свойства и методы, находящиеся в секции public, не имеют ограничений на видимость. Они доступны из других функций и методов объектов, как в данном модуле, так и во всех прочих, ссылающихся на него.
- 2. Поля, свойства и методы, находящиеся в секции private, доступны только в методах класса и в функциях, содержащихся в том же модуле, что и описываемый класс. Такая директива позволяет скрыть детали внутренней реализации класса от всех. Элементы из секции private можно изменять, и это не будет сказываться на программах, работающих с объектами этого класса. Обратиться к ним можно, только переписав содержащий их модуль.
- 3. Поля, свойства и методы, находящиеся в секции protected, доступны только внутри классов, являющихся потомками данного, в том числе и в других модулях. Такие элементы особенно необходимы дня разработчиков новых компонентов — потомков уже существующих.
- 4. Область видимости published имеет особое значение для интерфейса визуального проектирования. В этой секции должны быть собраны те свойства объекта, которые будут видны не только во время исполнения приложения, но и из среды разработки. Все свойства компонентов, доступные через Инспектор объектов, являются их опубликованными свойствами. Во время выполнения такие свойства общедоступны, как и public.
Пример, иллюстрирующий первые три варианта областей видимости:
unit First; interface type
tClassI = class public
procedure Methodl; private
procedure Method2; protected
procedure Method3; end;
procedure TestProcI; begin
Obj1:= tClassI .Create;
Оbj1.Methodl; //допустимо Obj1.Method2; //допустимо Obj1.Method3; //недопустимо Objl.Free; end;
end. // unit First
unit Second; interface uses First;
tClass2 = class(tClassl) procedure Method4; end;
var Obj2 : tClass2;
Methodl; //допустимо Method2; //недопустимо Method3; //допустимо
Obj2 .Methodl; //допустимо Obj2 .Method2; //недопустимо Obj2 .Method3; //недопустимо Obj2 .Free;
end. //unit Second
При описании дочернего класса можно переносить методы и свойства из одной сферы видимости в другую, не переписывая их заново и даже не описывая — достаточно указать новую сферу видимости наследуемого метода или свойства в описании дочернего класса. Разумеется, если вы поместили свойство в область private, «достать» его оттуда в потомках возможности уже нет.
СОЗДАНИЕ И УНИЧТОЖЕНИЕ ОБЪЕКТОВ
В Delphi объекты могут быть только динамическими! Любая переменная объектного типа есть указатель, причем для доступа к данным, на которые ссылается указатель объекта не нужно применять символ л .
Для выделения памяти экземпляру любой динамической переменной используется процедура New, для уничтожения — процедура Dispose. С объектами эти процедуры не используются: объект создается специальным методом — конструктором, а уничтожается специальным методом — деструктором:
AMyObject := tMyClass.Create; //создание объекта класса tMyClass
// действия с созданным объектом
AMyObject.Destroy; //уничтожение объекта AMyObject
Конструктор — это специальный метод, заголовок которого начинается зарезервированным словом constructor. Функция конструктора заключается в выделении памяти под экземпляр класса (объект) и в установлении связи между созданным объектом и специальной информацией о классе. В теле конструктора можно расположить любые операторы, которые необходимо выполнить при создании объекта, например, присвоить полям начальные значения. В Delphi конструкторов у класса может быть несколько. Общепринято называть конструктор Create.
До передачи управления телу конструктора происходит собственно создание объекта — под него отводится память. Далее выполняется код конструктора, написанный программистом. Фактически конструктор — это функция, возвращающая созданный и проини-циализированный объект.
Конструктор создает новый экземпляр класса только в том случае, если перед его именем указано имя класса.
Если указать имя уже созданного объекта, он поведет себя по-другому: не создаст новый экземпляр, а только выполнит код, содержащийся в теле конструктора.
Конструктор класса-потомка практически всегда должен перекрывать конструктор класса-предка. Чтобы правильно проинициа-лизировать в создаваемом объекте поля, относящиеся к классу-пред-ку, нужно в начале кода конструктора вызвать конструктор предка с помощью зарезервированного слова inherited:
constructor tMyClass. Create;
// собственный код конструктора
Деструктор — это специальный виртуальный (см. ниже) метод, заголовок которого начинается зарезервированным словом destructor. Деструктор предназначен для удаления объектов. Типичное название деструктора — Destroy.
Для уничтожения объекта рекомендуется использовать метод Free (он есть у базового класса TObject), который первоначально проверяет указатель на объект (не равен ли он nil) и только затем вызывает деструктор Destroy:
Деструктор очень часто не переопределяется в классе-потомке. Но если его необходимо переопределить, то необходимо учитывать следующее. Деструктор Destroy любого класса перекрывает виртуальный деструктор базового класса TObject, поэтому деструктор потомка необходимо объявить с директивой override:
destructor Destroy: override;
При реализации перекрывающего деструктора потомка нужно в конце кода деструктора вызвать деструктор предка с помощью зарезервированного слова inherited:
// собственный код деструктора inherited Destroy; end;
Классическое правило объектно-ориентированного программирования утверждает, что для обеспечения надежности нежелателен прямой доступ к полям объекта: чтение и обновление их содержимого должно производиться посредством вызова соответствующих методов. Это правило и называется инкапсуляцией. В Delphi пользователь объекта может быть полностью отгорожен от полей при помощи свойств.
Обычно свойство определяется тремя своими элементами: полем и двумя методами, которые осуществляют его чтение (запись):
function GetAProperty: tSomeType;
procedure SetAProperty(ANewValue: tSomeType);
property AProperty: tSomeType read GetAProperty write SetAProperty;
В контексте свойства слова read и write являются зарезервированными. Для доступа к значению свойства AProperty явно достаточно написать
и компилятор оттранслирует эти операторы в вызовы методов.
В методах, входящих в состав свойств, может осуществляться проверка устанавливаемой величины на попадание в допустимый диапазон значений, и вызов других процедур, зависящих от вносимых изменений. Если же потребности в специальных процедурах чтения и (или) записи нет, вместо имен методов можно применять имена полей.
Рассмотрим следующий пример:
tPropCIass = class(tObject) fValue: Integer; procedure DoSomething; function Correct(AValue: lnteger):Boolean;
procedure SetValue(NewValue: Integer); property AValue: Integer read fValue write SetValue;
procedure tPropClass.SetValue(NewValue: Integer);
if (NewValueofValue) and Correct (NewValue) then fValue := NewValue;
В этом примере чтение значения свойства AValue означает просто чтение поля fValue. Зато при присвоении ему значения внутри SetValue вызывается сразу два метода.
Если свойство должно только читаться или только записываться, в его описании может присутствовать только соответствующий метод:
property AProperty: tSomeType read GetValue;
Значение свойства AProperty можно лишь прочитать; попытки присвоить AProperty значение вызовет ошибку компиляции.
Для присвоения свойству значения по умолчанию используется ключевое слово default:
property Visible: Boolean read fVisible write SetVisible default True
Свойство может быть и векторным; в этом случае оно внешне выглядит как массив:
property APoints[lndex: IntegerftPoint read GetPoint write SetPoint;
На самом деле, среди полей класса может и не быть поля-массива. При помощи свойств вся обработка обращений к внутренним структурам класса может быть замаскирована.
Для векторного свойства необходимо описать не только тип элементов массива, но также имя и тип индекса. После ключевых слов read и write в этом случае должны стоять имена методов — использование здесь имени поля типа массив недопустимо. Метод чтения значения векторного свойства должен быть описан как функция, возвращающая значение того же типа, что и элементы свойства, и имеющая единственный параметр: того же типа и с тем же именем, что и индекс свойства:
function GetPoint(lndex: Integer): TPoint;
Метод записи значения в такое свойство должен первым параметром иметь индекс, а вторым — переменную нужного типа (которая может быть передана как по ссылке, так и по значению):
procedure SetPoint(lndex: Integer; NewPoint: tPoint);
У векторных свойств есть еще одна важная особенность. Некоторые классы в Delphi (списки, наборы строк) «построены» вокруг векторного свойства. Основной метод такого класса дает доступ к некоторому массиву, а все остальные методы являются вспомогательными. Специально для облегчения работы в этом случае векторное свойство может быть описано как default:
property Strings[lndex: Integer]: string read Get write Put; default;
Когда у объекта есть такое свойство (его называют векторным свойством по умолчанию), то можно его не упоминать, а ставить индекс в квадратных скобках прямо у имени объекта:
var AMyObject: tMyClass;
AMyObject.Strings[1] := ‘First’; //первый способ AMyObject[2] := ‘Second’; //второй способ • • •
Будьте внимательны, применяя зарезервированное слово default, — для обычных и векторных свойств оно употребляется в разных случаях и с различным синтаксисом.
О роли свойств в Delphi красноречиво говорит следующий факт: у всех имеющихся в распоряжении стандартных классов 100% полей недоступны (помещены в секцию private) и заменены базирующимися на них свойствами. Рекомендуем при разработке своих классов придерживаться этого же правила.
Принцип наследования позволяет объявить класс
являющийся потомком или дочерним классом старого класса, называемого предком или родительским классом, и добавить к нему новые поля, методы и свойства.
В Delphi все классы являются потомками класса TObject. При построении дочернего класс прямо от TObject в определении его можно не упоминать.
Следующие объявления одинаково верны:
tMyObject = class(TObject); tMyObject = class;
Первый вариант предпочтительнее, хотя он и более длинный, — для устранения возможных неоднозначностей.
Класс TObject несет очень серьезную нагрузку и будет рассмотрен отдельно.
Унаследованные от предка поля и методы доступны в дочернем классе; если имеет место совпадение имен методов, то говорят, что они перекрываются.
По тому, какие действия происходят при вызове, методы делятся на три группы:
- 1) статические;
- 2) виртуальные (virtual) и динамические (dynamic);
- 3) перегружаемые (overload) методы.
Статические методы, а также любые поля в объектах-потомках ведут себя одинаково: можно без ограничений перекрывать старые имена, и при этом изменять тип методов. Перекрытое поле предка недоступно в потомке. Перекрытый метод доступен при указании зарезервированного слова inherited. Методы объектов по умолчанию являются статическими — их адрес определяется еще на стадии компиляции проекта. Они вызываются быстрее всего.
Принципиально отличаются от статических виртуальные и динамические методы (директива virtual или dynamic). Их адрес определяется во время выполнения программы по специальной таблице. С точки зрения наследования методы этих двух видов одинаковы: они могут быть перекрыты в дочернем классе только одноименными методами, имеющими тот же тип.
В Delphi множественного наследования нет. Если вы хотите, чтобы новый класс объединял свойства нескольких, породите классы-предки один от другого или включите в класс несколько полей, соответствующих желаемым классам.
ВИРТУАЛЬНЫЕ И ДИНАМИЧЕСКИЕ МЕТОДЫ
Рассмотрим пример. Пусть у нас имеются некое обобщенное поле для хранения данных — абстрактный класс tField и три его потомка — для хранения строк, целых и вещественных чисел:
function GetData: string; virtual; abstract;
tStringField = class(tField) fData : string;
function GetData: string; override;
function GetData: string; override;
function GetData: string; override;
В этом примере классы содержат разнотипные данные и «умеют» сообщать о значении этих данных текстовой строкой (при помощи метода GetData). Внешняя по отношению к ним процедура Show Data получает объект в виде параметра и показывает эту строку.
Правила контроля соответствия типов (typecasting) языка Delphi гласят, что объекту как указателю на экземпляр объектного типа может быть присвоен адрес любого экземпляра любого из дочерних типов. В процедуре ShowData параметр описан как tField — это значит, что в нее можно передавать объекты классов и tStringField, и tlntegerField, и tExtendedField, и любого другого потомка tField.
Но чей метод GetData при этом будет вызван? Тот, который соответствует классу фактически переданного объекта. Этот принцип называется полиморфизмом и он, пожалуй, представляет собой наиболее важный принцип ООП. Например, чтобы смоделировать некоторую совокупность явлений или процессов средствами ООП, нужно выделить их самые общие, типовые черты. Те из них, которые не изменяют своего содержания, должны быть реализованы в виде статических методов. Те же, которые варьируются при переходе от общего к частному, лучше облечь в форму виртуальных методов. Основные, «родовые» черты (методы) нужно описать в классе-пред-ке и затем перекрывать их в классах-потомках.
При вызове виртуальных и динамических методов адрес определяется не во время компиляции, а во время выполнения — это называется поздним связыванием (late binding). Позднее связывание реализуется с помощью таблицы виртуальных методов (Virtual Method Table, VMT) и таблицы динамических методов (Dynamic Method Table, DMT).
Разница между виртуальными и динамическими методами заключается в особенности поиска адреса. Когда компилятор встречает обращение к виртуальному методу, он подставляет вместо обращения к конкретному адресу код, который обращается к VMT этого объекта и извлекает оттуда нужный адрес. Такая таблица одна для каждого класса (объектного типа). В ней хранятся адреса всех виртуальных методов класса, независимо от того, унаследованы ли они от предка или перекрыты. Отсюда и достоинства, и недостатки виртуальных методов: они вызываются сравнительно быстро (но медленнее статических), однако для хранения указателей на них требуется большое количество памяти.
Динамические методы вызываются медленнее, но позволяют более экономно расходовать память. Каждому динамическому методу системой присваивается уникальный индекс. В таблице динамических методов класса хранятся индексы и адреса только тех динамических методов, которые описаны в данном классе. При вызове динамического метода происходит поиск в этой таблице; в случае неудачи просматриваются таблицы DMT всех классов-предков в порядке иерархии и, наконец, DMT класса TObject, где имеется стандартный обработчик вызова динамических методов. Экономия памяти налицо.
Для перекрытия и виртуальных, и динамических методов служит директива override, с помощью которой (и только с ней!) можно переопределять оба этих типа методов.
tClassI = class fField 1: Integer; fField2: Longint; procedure stMet; procedure vrMetl; virtual; procedure vrMet2; virtual; procedure dnMetl; dynamic; procedure dnMet2; dynamic; end;
tClass2 = class(tClassl) procedure stMet; procedure vrMetl; override; procedure dnMetl; override; end;
Первый метод класса tClass2 создается заново, остальные два — перекрываются. Попытка применить директиву override к статическому методу вызовет ошибку компиляции.
ПРИМЕР РЕАЛИЗАЦИИ ПОЛИМОРФИЗМА ДЛЯ ИЕРАРХИИ ГРАФИЧЕСКИХ ОБЪЕКТОВ
Рассмотрим описание и реализацию классов «Точка» и «Окружность» для «рисования» на экране точки и окружности.
// тип — координата точки на экране
// класс — точка на экране дисплея
// поля — координаты точки на экране // поле — цвет рисования точки
// Описание класса 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)
property Radius: tCoord read fRadius write fRadius; //св-во — радиус procedure Show; //метод высвечивания окружности
procedure Hide; //метод гашения окружности
procedure MoveTo (NewX,NewY:tCoord) //перемещение окружности
// Реализация методов класса tPoint
// «Рисование» точки
Writeln(‘Pi/icyio точку (‘, fx,fy,’) цветом fColor);
// «Стирание» точки Л/гйе1п(‘Стираю точку (‘, fx, ‘,fy,’) цвета fColor);
procedure tPoint.MoveTo(NewX, NewY: tCoord);
fX := NewX; fY := NewY;
// Реализация методов класса tCircle
Writeln(‘PHcyK) окружность с центром (‘, fx,fy,’) радиуса fRadius,’ цветом fColor);
procedure tCircle.Hide; begin
Л/гйе1п(‘Стираю окружность с центром (‘, fx,’,’, fy,’) радиуса ‘, fRadius,’ цвета ‘, fColor);
procedure tCircle.MoveTo (NewX,NewY: tCoord);
fX := NewX; fY := NewY;
Обратите внимание, что методы MoveTo классов tPoint и tCircle реализуются одинаково за исключением того, что в tPoint. MoveTo используются tPoint.Hide и tPoint.Show, а в tCircle.MoveTo используются tCircle.Hide и tCircle.Show. Взаимодействие методов классов tPoint и tCircle можно проиллюстрировать схемой, приведенной на рис. П3.1.
Рис. П3.1. Взаимодействие методов классов tPoint и tCircle
Поскольку методы реализуются одинаково, можно попытаться унаследовать метод МоуеТо от класса 1РотЕ При этом возникает следующая ситуация.
При компиляции метода 1РотСМоуеТо в него будут включены ссылки на коды методов 1РотС81кж и 1РотЕНк1е. Так как метод с именем МоуеТо не определен в классе 1Слгс1е, то компилятор обращается к родительскому типу и ищет в нем метод с этим именем. Если метод найден, то адрес родительского метода заменяет имя в исходном коде потомка.
Следовательно, при вызове 1СДс1е. МоуеТо в программу будут включены коды 1РотС МоуеТо (то есть класс 1:Сцс1е будет использовать метод так, как он откомпилирован). В этом случае процедура 1Сцс1е. МоуеТо будет работать неверно.
Структура вызовов методов при таком наследовании приведена на рис. П3.2.
Рис. П3.2. Структура вызовов методов при наследовании метода
MoveTo от класса tPoint
Для правильного вызова методов процедуры Show и Hide должны быть объявлены виртуальными, так как только в этом случае класс tCircle может наследовать метод MoveTo у типа tPoint. Это становится возможным потому, что подключение виртуальных методов Show и Hide к процедуре MoveTo будет осуществляться во время выполнения программы, и будут подключаться методы, определенные для типа tCircle (tCircle.Show и tCircle.Hide).
При использовании виртуальных методов Show и Hide взаимодействие методов классов tPoint и tCircle можно проиллюстрировать схемой, приведенной на рис. ПЗ.З.
Рис. ПЗ.З. Структура вызовов методов при использовании виртуальных методов Show и Hide
Подключение виртуальных методов осуществляется с помощью специальной таблицы виртуальных методов (VMT).
Описание классов «Точка» и «Окружность» с использованием виртуальных методов:
// Описание класса 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;
property Radius: tCoord read fRadius write fRadius; procedure Show; override; procedure Hide; override; end;
Перегружаемые методы нельзя назвать антагонистом двух предыдущих: и статические и динамические методы могут быть перегружаемыми. Перегрузка нужна, чтобы выполнить одинаковые или похожие действия с разнотипными данными.
tClassI = class i: Extended;
procedure SetData(AValue : Extended);
tClass2 = class(tClassl) j: Integer;
procedure SetData(AValue : Integer);
Попытка вызова методов
вызовет ошибку компиляции на первой из двух строк. Для компилятора внутри Obj2 статический метод с параметром типа Extended перекрыт, и он его не «признает».
Объявить методы виртуальными нельзя, так как тип и количество параметров в одноименных виртуальных методах должны совпадать. Чтобы указанные вызовы были верными, необходимо объявить методы перегружаемыми, для чего используется директива overload:
tClassI = class i: Extended:
procedure SetData(AValue : Extended); overload; end;
tClass2 = class(tClassl) j: Integer;
procedure SetData(AValue : Integer); overload; end;
Объявив метод Set Data перегружаемым, в программе можно использовать обе его реализации одновременно. Это возможно потому, что компилятор определяет тип передаваемого параметра (целый или с плавающей точкой) и в зависимости от этого подставляет вызов соответствующего метода.
Можно перегружать и виртуальные методы. В этом случае необходимо добавить директиву reintroduce:
tClassI = class i : Extended;
procedure SetData(AValue : Extended); virtual; overload; end;
tClass2 = class(tClassl) j: Integer;
procedure SetData(AValue : Integer); reintroduce; overload; end;
На перегрузку методов накладывается ограничение — нельзя перегружать методы, находящиеся в области видимости published.
Абстрактными называются методы, которые определены в классе, но не содержат никаких действий, никогда не вызываются и обяза-
тельно должны быть переопределены в потомках класса. Абстрактными могут быть только виртуальные и динамические методы. В Delphi есть одноименная директива (abstract), указываемая при описании метода:
procedure NeverCallMe; virtual; abstract;
Никакого кода для этого метода писать не нужно. Вызов абстрактного метода приведет к созданию исключительной ситуации Е Abstract Error.
Пример с классом tField из параграфа «Полиморфизм» поясняет, для чего нужны абстрактные методы. В данном случае класс tField не используется сам по себе; его основное предназначение — быть родоначальником иерархии конкретных классов-«полей» и дать возможность абстрагироваться от частностей. Хотя параметр процедуры ShowData и описан как tField, но если передать в нее объект этого класса, произойдет исключительная ситуация вызова абстрактного метода.
Рассмотрим уже знакомый пример из параграфа «Полиморфизм»:
tClassI = class fFieldl: Integer; fField2: Longint; procedure stMet; procedure vrMetl; virtual; procedure vrMet2; virtual; procedure dnMetl; dynamic; procedure dnMet2; dynamic;
tClass2 = class(tClassl) procedure stMet; procedure vrMetl; override; procedure dnMetl; override;
Внутренняя структура объектов Objl и Obj2 приведена на рис. П3.4.
Первое поле каждого экземпляра объекта содержит указатель на его класс. Класс как структура состоит из двух частей. Начиная с адреса, на который ссылается указатель на класс, располагается таблица виртуальных методов, содержащая адреса всех виртуальных методов класса, включая унаследованные от предков.
Перед таблицей виртуальных методов расположена специальная структура, которая называется информацией о типе времени выполнения (runtime type information, RTTI). В ней содержатся данные, полностью характеризующие класс: его имя, размер экземпляра, указатели на класс-предок, на имя класса и т. д. На рисунке она показана одним блоком, а ее содержимое расшифровано ниже.
Указатель на класс tClassl
Указатель на класс
Поле fFieldl Поле fField2
VMT класса tClassl
DMT класса tClassl
RTTI класса tClassl
Число динамических методов: 2
Индекс tClassl .dnMetl (-1)
Индекс tClassl ,dnMet2 (-2)
(й) tClassl .dnMetl
VMT класса tClass2
DMT класса tClass2
RTTI класса tClass2
Число динамических методов: 1
Индекс tClass2.dnMetl (-1)
(2; tClassl ,vrMet2
Рис. П3.4. Внутренняя структура объектов Obj 1 и Obj2
Одно из полей структуры содержит адрес таблицы динамических методов класса (DMT). Таблица имеет следующий формат — в начале слово, содержащее количество элементов таблицы; затем — слова, соответствующие индексам методов. Нумерация индексов начинается со значения — 1 и идет по убывающей. После индексов идут собственно адреса динамических методов.
Обратите внимание, что DMT объекта Obj 1 состоит из двух элементов, Obj2 — из одного элемента, соответствующего перекрытому методу dnMetl. В случае вызова метода Obj2.dnMet2 индекс не будет найден в таблице DMT Obj2, поиск продолжится в DMT объекта Obj 1. Именно так экономится память при использовании динамических методов.
Напомним, что указатель на класс указывает на адрес первого виртуального метода. Служебные данные размещаются перед таблицей VMT, то есть с отрицательным смещением. Эти смещения описаны в модуле SYSTEM.PAS:
= -52; = -48; = -44; = -40; -36;
Поля УггйОупагтсТаЫе, Уггй018ра1с11 и У1УйОеГаиИ:Напб1ег отвечают за вызов динамических методов. Поля угтп№у1п81апсе, угШЕгее1п81апсе и УгШЭез^оу содержат адреса процедур создания и уничтожения экземпляра класса. Поля утПшЛаЫе, УпйА^оТаЫе, уш15аГеСа11Ехсер1юп введены для обеспечения совместимости с моделью объектов СОМ. Остальные поля доступны через методы класса ТСИуесЕ в Ое1р1м информация этой таблицы играет важную роль и может использоваться программистом неявно.
ПРОВЕРКА СОВМЕСТИМОСТИ И ПРИВЕДЕНИЕ ОБЪЕКТНЫХ ТИПОВ
В языке определены два оператора: к и аз, неявно обращающиеся к информации о классе.
Оператор к предназначен для проверки совместимости по присваиванию экземпляра объекта с заданным классом. Выражение вида:
AnObject is TObjectType
принимает значение True только в том случае, если объект AnObject совместим по присваиванию с TObjectType, то есть является объектом этого класса или одного из классов, порожденных от него. Кстати, определенная проверка происходит еще при компиляции: если фактические объект и класс несовместимы, компилятор выдаст ошибку в этом операторе.
Оператор as введен в язык для приведения объектных типов. С его помощью можно рассматривать экземпляр объекта как принадлежащий к другому совместимому типу:
with ASomeObject as tAnotherType do .
Использование оператора as отличается от стандартного способа приведения типов с помощью конструкции
наличием проверки на совместимость типов во время выполнения (как в операторе is): попытка приведения к несовместимому типу приводит к возникновению исключительной ситуации EInvalidCast.
После применения оператора as сам объект остается неизменным, но вызываются только те его методы, которые есть у присваиваемого класса. Присваиваемый фактически тип должен быть известен на стадии компиляции, поэтому на месте TObjectType (после is) и tAnotherType (после as) не может стоять переменная-указатель на класс.
УКАЗАТЕЛЬ НА КЛАСС. МЕТОДЫ КЛАССА
Информация, описывающая класс, создается и размещается в памяти на этапе компиляции. Возникает вопрос: можно ли получить доступ к ней, не создавая экземпляр класса (объект)? Да, можно.
Доступ к информации класса вне методов этого класса можно получить, описав соответствующий указатель, который называется указателем на класс, или указателем на объектный тип (class reference). Он описывается при помощи зарезервированных слов class of. Например, указатель на класс TObject описан в модуле SYSTEM.PAS и называется tClass:
TObject = class; tClass = class of TObject;
Указатели на классы подчиняются правилам приведения объектных типов. Указатель на класс-предок может ссылаться и на любые дочерние классы; обратное невозможно.
С указателем на класс тесно связано понятие методов класса. Такие методы можно вызывать без создания экземпляра объекта — с указанием имени класса, в котором они описаны. Перед описанием метода класса нужно поставить зарезервированное слово class:
tMyClass = class (TObject) class function GetSize: string; end;
// обращение к методу класса до создания объекта этого класса
Разумеется, методы класса не могут использовать значения, содержащиеся в полях: ведь экземпляра-то не существует! Возникает вопрос: для чего тогда нужны такие методы? Важнейшие методы класса определены в самом классе TObject: они позволяют извлечь из внутренней структуры класса практически всю необходимую информацию.
Некоторые важные методы класса TObject приведены в табл. П3.1.
Таблица ПЗ. I
Основные методы класса TObject
class function Classlnfo: Pointer;
Возвращает указатель на структуру с дополнительными данными об опубликованных методах и свойствах
class function ClassName: ShortString;
Возвращает имя класса
class function ClassNamels (const Name: ShortString): Boolean;
Возвращает True, если имя класса равно заданному
class function Class Parent: TClass;
Возвращает указатель на родительский класс (для TObject возвращает nil)
class function InheritsFrom
(AClass: TClass): Boolean;
Возвращает True, если данный класс порожден от класса AClass
class function Initlnstance (Instance: Pointer): TObject;
Инициализирует экземпляр класса
class function InstanceSize: Longint;
Возвращает размер экземпляра класса
MethodAddress(const Name: ShortString): Pointer;
Возвращает адрес метода по его имени (только для опубликованных методов)
class function MethodName (Address: Pointer): ShortString;
Возвращает имя метода по его адресу (только для опубликованных методов)
class function Newlnstance: TObject; virtual;
Создает новый экземпляр объекта.
В частности, эта функция вызывается внутри конструктора
function ClassType: TClass;
Возвращает указатель на класс данного объекта
Конструктор. Создает новый экземпляр класса и инициирует обработчик исключительных ситуаций
Вызывается сразу после создания экземпляра класса
Вызывается перед уничтожением экземпляра
destructor Destroy; virtual;
Деструктор. Производит действия по уничтожению экземпляра класса
procedure Default Handler (var Message); virtual;
Обработчик сообщений по умолчанию.
В TObject не содержит ничего, кроме кода возврата
procedure Dispatch(var Message);
Вызывает методы обработки сообщений в зависимости от значения параметра Message
Name: String): Pointer;
Возвращает адрес поля вызвавшего объекта с заданным именем
Используется вместо деструктора. Проверяет передаваемый деструктору указатель на экземпляр
procedure Free Instance; virtual;
Уничтожает экземпляр объекта. Вызывается внутри деструктора
В следующем примере переменная Object Ref является указателем на класс; он по очереди указывает на TObject и TMyObject. Посредством этой переменной-указателя вызывается функция класса ClassName:
TMyObjClass = class of TObject;
s := ObjectRef .ClassName; //s :=T0bject’
s:= ObjectRef .ClassName; //s : =’TMyObject’
Виртуальные методы AfterConstruction и Before Destruction вызываются сразу после создания экземпляра класса и непосредственно перед его уничтожением. Их можно использовать, если по каким-либо причинам не хватает собственно конструктора и деструктора.
Удобно использовать метод Free для уничтожения экземпляра класса, при этом переопределяется метод Destroy, в котором описываются все действия по уничтожению экземпляра, например освобождение выделенной памяти или закрытие файлов. При вызове метода Free проверяется передаваемый указатель на экземпляр и если он не равен nil, то вызывается метод Destroy и происходит уничтожение объекта.
Методы MethodName и MethodAddress используются для получения имени или адреса только опубликованных методов, это связано с тем, что система Delphi хранит имена только таких методов. Так как директива published предназначена для описания свойств, размещаемых в инспекторе объектов, где указываются их имена.
Если метод MethodName вызывается для указателя на неопубликованный метод, то возвращается пустая строка.
При передаче методу MethodAddress в качестве параметра имени неопубликованного или несуществующего метода возвращается значение nil.
Метод FieldAddress используется для получения адреса в памяти опубликованного поля объекта. Если в качестве параметра передается имя неопубликованного или несуществующего поля, то возвращается значение nil.
Синергия ответы Объектно-ориентированное программирование. ОТВЕТЫ на 100 вопросов. 1. Как называется специализированный метод класса, предназначенный для
1.
Как называется специализированный метод класса, предназначенный для
создания нового экземпляра?
Конструктор
2.
Что из перечисленного может быть отнесено к преимуществам шаблона
«строитель»?
В сравнении с фабричным методом придает дополнительную гибкость процессу конструирования объектов
Изолирует код конструирования объектов
Позволяет изменять внутреннее представление объекта
Не требует дополнительной иерархии классов
3.
Что из перечисленного может быть отнесено к членам класса?
Вложенный класс
Поле
Метод
Объект
4.
Что из перечисленного может быть отнесено к недостаткам шаблона
«отложенная инициализация»?
Порождает дополнительные ошибки при многопоточной работе
Вносит дополнительную задержку при обращении к объекту
Не позволяет явным образом задать порядок инициализации
Замедляет инициализацию объектов
5.
Что из перечисленного может быть отнесено к преимуществам шаблона
«заместитель»?
Не требует создания новых объектов
Позволяет придать объектам дополнительный функционал
Позволяет инициализировать объекты по требованию
Сокращает время отклика системы
6.
Верно ли, что MVC позволяет визуализировать одни и те же данные разными
способами без изменения модели?
Нет
Да
7.
Сочетание объединения всех свойств объекта, определяющих его состояние и
поведение, в единую абстракцию и ограничение доступа к реализации этих свойств
называется
Инкапсуляция
Параллелизм;
Полиморфизм;
Абстрагирование;
8.
Каким образом будет выглядеть инструкция, указывающая на принадлежность
описываемого класса к пакету test, который в свою очередь является подпакетом класса
bigtest?
package bigtest.test;
9.
Верно ли, что абстрактный класс не может содержать реализаций методов?
Да
Нет
10.
Верно ли, что в активной модели MVC об изменениях данных представление
оповещает контроллер?
Да
Нет
11.
Верно ли, что в пассивной модели MVC об изменениях данных представление
оповещает контроллер?
Да
Нет
12.
Как называется один из фундаментальных механизмов объектно-ориентированного
программирования, позволяющий классу ограничивать доступ к своим членам?
Инкапсуляция
13.
Верно ли, что концепция MVC включает в себя ровно три структурных
составляющих: модель, вид и контроллер?
Да
Нет
14.
Верно ли, что наиболее общим отношением между классами является
отношение зависимости?
Да
Нет
15.
Верно ли, что один абстрактный класс не может являться родительским по
отношению к другому абстрактному классу?
Да
Нет
16.
Верно ли, что основная цель использование концепции MVC – разграничение
логики работы и визуализации?
Да
Нет
17.
Верно ли, что основная цель использование концепции MVC – упрощение
архитектуру приложения?
Да
Нет
18.
Верно ли, что отношение агрегации может применяться для изображения
иерархических отношений между классами?
Да
Нет
19.
Верно ли, что понятие «суперкласс» является синонимом родительского
класса?
Нет
Да
20.
Верно ли, что понятия «вложенный класс» и «класс-член» являются
синонимами?
Да
Нет
21.
Верно ли, что программист не может управлять сборкой мусора?
Да
Нет
22.
Задачи какого рода решают основные шаблоны проектирования?
Повседневные задачи: переадресация работы, обеспечение слабой связности системы и так далее
Абстрагирование процесса создания экземпляров класса
Создание различных структур, изменяющих поведение уже существующих объектов
Определяют алгоритмы и способы взаимодействия объектов между собой
23.
Задачи какого рода решают поведенческие шаблоны проектирования?
Повседневные задачи: переадресация работы, обеспечение слабой связности системы и так далее
Создание различных структур, изменяющих поведение уже существующих объектов
Абстрагирование процесса создания экземпляров класса
Определяют алгоритмы и способы взаимодействия объектов между собой
24.
Задачи какого рода решают порождающие шаблоны проектирования?
Повседневные задачи: переадресация работы, обеспечение слабой связности системы и так далее
Абстрагирование процесса создания экземпляров класса
Создание различных структур, изменяющих поведение уже существующих объектов
Определяют алгоритмы и способы взаимодействия объектов между собой
25.
Задачи какого рода решают структурные шаблоны проектирования?
Определяют алгоритмы и способы взаимодействия объектов между собой
Создание различных структур, изменяющих поведение уже существующих объектов
Повседневные задачи: переадресация работы, обеспечение слабой связности системы и так далее
Абстрагирование процесса создания экземпляров класса
26.
Какая из парадигм программирования является ведующей в языке Java?
Объектно-ориентированная
Процедурная
Событийно-ориентированная
Компонентно-ориентированная
27.
Какая команда используется для выполения байт-кода?
java javac javar javarun
28.
Какие группы шаблонов проектирования обычно выделяют?
Основные
Порождающие
Структурные
Поведенческие
29.
Какие задачи позволяет решать ключевое слово final?
Создание ненаследуемых методов
Создание абстрактных классов
Создание непереопределяемых методов
Создание неизменяемых полей
Создание классов, которые не могут иметь наследников
30.
Какие из перечисленных требований должны выполняться для создания
шаблона «неизменяемый объект» в Java?
Все поля класса должны иметь модификатор private
Все методы класса должны быть объявлены с модификатором static
Ссылка this не должна передаваться вовне во время конструирования объекта
Класс должен быть объявлен с модификатором final
31.
Какие из перечисленных утверждений касательно высокоуровневых
шаблонов проектирования верны?
Высокоуровневые шаблоны также называют архитектурными
Высокоуровневые шаблоны являются универсальными решениями
Несмотря на то, что высокоуровневые шаблоны являются универсальными, они учитывают специфические особенности конкретных языков программирования
Высокоуровневые шаблоны также называют идиомами
32.
Какие из перечисленных утверждений касательно низкоуровневых шаблонов
проектирования верны?
Низкоуровневые шаблоны учитывают специфику языка программирования
Низкоуровневые шаблоны также называют идиомами
Низкоуровневые шаблоны являются универсальными
Низкоуровневые шаблоны не используются в прикладных разработках
33.
Какие из предлагаемых утверждений верны?
Статическим называется член класса, доступ к которому возможен без создания экземпляра
Создание статического метода возможно только при использовании ключевого слова static
Статические поля являются общими для всех экземпляров класса
Статические поля недоступны экземплярам класса
34.
Какие типы связей используют в UML?
Зависимость
Ассоциация
Обобщение
Реализация
35.
Какие типы сущностей выделяют в UML?
Поведенческие
Аннотирующие
Структурные
Группирующие
36.
Каково основное назначение статического импорта?
Позволяет JVM эффективно перенаправлять запросы к классам из других пакетов, принимая во внимание их статическую структуру
Позволяет обращаться к статическим членам без явного указания класса-владельца
Добавляет в область видимости описываемого класса все статические структуры из указанного пакета
37.
Какое из перечисленных утверждений верно?
UML создан путем объединения всех существующих подходов проектирования того времени
UML создан на основе наиболее популярных подходов проектирования того времени
UML является исторически первым языком моделирования
38.
Какое ключевое слово может быть использовано для обращения экземпляра
класса к самому себе?
this super instanceof
own
39.
Какое количество секций может содержать блок описания интерфейса на
UML-диаграмме классов?
Одну
Две
Три
Четыре
40.
Какое количество секций может содержать блок описания класса на UML-
диаграмме классов?
Одну
Три
Две
Четыре
41.
Как расшифровывается аббревиатура UML?
Unified Modeling Language
Universally Modeling Language
Unified Modern Language
Universally Modern Luggage
42.
Что из перечисленного может быть отнесено к недостаткам шаблона
«отложенная инициализация»?
Не позволяет явным образом задать порядок инициализации
Вносит дополнительную задержку при обращении к объекту
Порождает дополнительные ошибки при многопоточной работе
Замедляет инициализацию объектов
43.
Что из перечисленного может быть отнесено к преимуществам шаблона
«адаптер»?
Увеличивает степень независимости системы от сторонних библиотек
Смена библиотек не требует изменения всей системы
Ускорение начальной инициализации
Минимизация задержки при обращении
44.
Что из перечисленного может быть отнесено к преимуществам шаблона
«декоратор»?
Позволяет динамически добавлять объектам новые обязанности
Упрощает структуру отдельных классов
Сокращает количество имен в адресном пространстве
Не требует дополнительной иерархии классов
45.
Что из перечисленного может быть отнесено к преимуществам шаблона
«делегирование»?
Позволяет изменить поведение избегая наследования
Повышает степень абстракции
Положительно сказывается на времени обработки запроса системой
Сокращает число элементов в адресном пространстве
46.
Что из перечисленного может быть отнесено к преимуществам шаблона
«заместитель»?
Позволяет инициализировать объекты по требованию
Позволяет придать объектам дополнительный функционал
Сокращает время отклика системы
Не требует создания новых объектов
47.
Что из перечисленного может быть отнесено к преимуществам шаблона
«итератор»?
Позволяет поддерживать единообразный интерфейс для доступа к элементам системы
Позволяет поддерживать несколько активных обходов коллекции
Итератор не должен владеть всей информацией о коллекции
Не требует дополнительной иерархии классов
48.
Что из перечисленного может быть отнесено к преимуществам шаблона
«команда»?
Позволяет параметризировать объекты выполняемым действием
Позволяет обрабатывать запросы как объекты
Упрощает иерархию классов
Уменьшает время отклика системы
49.
Что из перечисленного может быть отнесено к преимуществам шаблона
«компоновщик»?
Упрощает архитектуру приложения
Упрощает процедуру добавления новых компонентов в систему
Позволяет единообразно работать с составными и примитивными структурами
Позволяет ограничить типы используемых компонентов
50.
Что из перечисленного может быть отнесено к преимуществам шаблона
«мост»?
Позволяет отделять абстракцию от реализации
Чаще всего изменение реализации не требует перекомпиляции кода клиента
Чаще всего изменение абстракции не требует перекомпиляции кода клиента
Сокращает время доступа к объектам
51.
Что из перечисленного может быть отнесено к преимуществам шаблона
«наблюдатель»?
Сокращает цепочку вызовов, необходимую для уведомления наблюдателей об изменении состояния объекта
Абстрагирует связность объекта и наблюдателей
Позволяет обеспечить широковещательную рассылку уведомлений
Повышает степень повторной используемости кода
52.
Что из перечисленного может быть отнесено к преимуществам шаблона
«неизменяемый объект»?
Позволяет предотвратить ряд ошибок
Может быть использован в качестве ключа коллекции
Удобен для многопоточной работы
Положительно сказывается на скорости работы
53.
Что из перечисленного может быть отнесено к преимуществам шаблона
«одиночка»?
Сокращение числа имен в глобальном адресном пространстве
Предоставление глобальной точки доступа
Ускорение начальной инициализации
Минимизация задержки при обращении в силу статической сущности
54.
Что из перечисленного может быть отнесено к преимуществам шаблона
«посредник»?
Централизовывает управление
Обеспечивает слабую связность системы
Упрощает протоколы взаимодействия между компонентами
Повышает степень повторной используемости кода
55.
Что из перечисленного может быть отнесено к преимуществам шаблона
«прототип»?
Позволяет специфицировать новые объекты путем изменения значений
Позволяет сократить иерархию классов
Позволяет динамически конфигурировать приложение классами
Значительно уменьшает время, необходимое на создание новых объектов
56.
Что из перечисленного может быть отнесено к преимуществам шаблона
«состояние»?
Изолирует логику работы от реализации
Не требует дополнительных имен в адресном пространстве
Позволяет полностью скрыть информацию о классе-владельце
Позволяет избегать применения цепочек условных операторов
57.
Что из перечисленного может быть отнесено к преимуществам шаблона
«стратегия»?
Позволяет оперировать семейством алгоритмов
Предотвращает порождение большого числа подклассов
Сокращает цепочки условных операторов, реализующих сложное поведение системы
Позволяет динамически выбирать реализацию алгоритма
58.
Что из перечисленного может быть отнесено к преимуществам шаблона
«строитель»?
В сравнении с фабричным методом придает дополнительную гибкость процессу конструирования объектов
Изолирует код конструирования объектов
Позволяет изменять внутреннее представление объекта
Не требует дополнительной иерархии классов
59.
Что из перечисленного может быть отнесено к преимуществам шаблона
«фабричный метод»?
Положительно сказывается на скорости работы
Позволяет устанавливать связь между параллельными иерархиями классов
Сокращает количество имен в адресном пространстве
Позволяет сделать код создания объектов более универсальным
60.
Что из перечисленного может быть отнесено к преимуществам шаблона
«фасад»?
Изолирует клиентов от подсистем
В большинстве случаев приводит к сокращению числа имен в адресном пространстве клиентов
Облегчает устройство системы
Ограничивает доступ клиентов к подсистемам
61.
Что из перечисленного может быть отнесено к преимуществам шаблона
«хранитель»?
Позволяет сохранять внутренне состояние объекта
Упрощает структуру класса-владельца
Не раскрывает детали реализации класса-владельца
Позволяет восстанавливать состояние класса-владельца
62.
Что из перечисленного может быть отнесено к преимуществам шаблона
«цепочка ответственности»?
Гарантирует, что рано или поздно запрос будет обработан
Позволяет ослабить связи внутри системы
Не требует дополнительной иерархии классов
Придает дополнительную гибкость при распределении обязанностей
63.
Что из перечисленного может быть отнесено к преимуществам шаблона
«шаблонный метод»?
Помогает избегать дублирования повторяющихся конструкций
Позволяет динамически варьировать поведение системы
Позволяет оптимизировать передачу данных при вызове методов
Не требует дополнительной иерархии классов
64.
Что из перечисленного может быть отнесено к членам класса?
Вложенный класс
Метод
Объект
Поле
65.
Каким образом будет выглядеть команда для компиляции класса test,
описанного в файле test.java?
javac test.java
66.
Каким символом на диаграмме классов изображается тот факт, что атрибут
имеет бесконечную кратность?
*
67.
Каким символом на диаграмме классов изображается тот факт, что атрибут
имеет область видимости private?
—
68.
Каким символом на диаграмме классов изображается тот факт, что атрибут имеет
область видимости protected?
#
69.
Каким символом на диаграмме классов изображается тот факт, что атрибут имеет
область видимости public?
+
70.
Верно ли, что для создания десктруктора в Java используется блок finalize?
Да
Нет
71.
Как называется один из фундаментальных механизмов объектно-ориентированного
программирования, позволяющий одному классу расширять функциональность другого,
заимствуя при этом поля и методы?
Наследование
72.
Как называется один из фундаментальных механизмов объектно-ориентированного
программирования, позволяющий элементам с одинаковой спецификацией иметь различную
реализацию?
Полиморфизм
73.
Как называется специализированный метод класса, предназначенный для создания
нового экземпляра?
Конструктор
74.
Как называется специализированный метод класса, предназначенный для уничтожения
существующего экземпляра?
Деструктор
75.
Какое ключевое слово используется для указания класса-родителя?
super
76.
Какое ключевое слово используется для указания того, что класс реализует интерфейс?
implements
77.
Какое название получил механизм, позволяющий JVM работать с примитивными
типами данных как с объектами?
автоупаковка
78.
Какой вид будет иметь инструкция, добавляющая в область видимости описываемого
класса все классы из пакета test?
import test.*;
79.
Верно ли, что абстрактный класс не может иметь экземпляров?
Да
Нет
80.
Какой вид будет иметь инструкция, добавляющая в область видимости описываемого
класса класс test из пакета test?
import test.test;
81.
Какой вид будет иметь инструкция, указывающая на принадлежность описываемого
класса к пакету test?
package test;
82.
Как расшифровывается аббревиатура JDK?
Java Development Kit
83.
Как расшифровывается аббревиатура JRE?
Java Runtime Environment
84.
Как расшифровывается аббревиатура JVM?
Java Virtual Machine
85.
С помощью какой команды может быть создан вектор с именем test из элементов типа
TestEl?
Vector test = new Vector ();
86.
С помощью какой команды может быть создан массив с именем test из десяти
элементов типа int?
int[] test = new int[10];
87.
Как будет выглядеть команда для запуска класса Test, готовый байт код которого
содержится в файле Test.class
java Test
88.
Как называется организация, регулирующая развитие UML?
OMG
ANSI
ACM
NASA
89. Поля (данные-члены) класса могут быть
только целыми числами; любыми встроенными типами; любого определенного в программе типа; любого определенного в программе типа и указателем на объект этого же класса;
90. Верно ли, что MVC позволяет подключать к одному виду несколько
контроллеров?
Да
Нет
91. Ограничение, накладываемое на свойства объектов для сужения или
препятствия взаимозаменяемости абстракций различных типов называется
Типизация
Абстрагирование;
Полиморфизм;
Инкапсуляция.
92.Как называется один из фундаментальных механизмов объектно-
ориентированного программирования, позволяющий элементам с одинаковой
спецификацией иметь различную реализацию?
Полиморфизм
93.Какое название получил механизм, позволяющий JVM работать с примитивными
типами данных как с объектами?
автоупаковка
94.Какие из перечисленных утверждений касательно низкоуровневых шаблонов
проектирования верны?
Низкоуровневые шаблоны также называют идиомами
Низкоуровневые шаблоны не используются в прикладных разработках
Низкоуровневые шаблоны являются универсальными
Низкоуровневые шаблоны учитывают специфику языка программирования
95. Верно ли, что Java позволяет создавать массив заранее известных величин?
Да
Нет
96. Верно ли утверждение о том, что Java является императивным языком
программирования?
Да
Нет
97. Верно ли утверждение о том, что Java является декларативным языком
программирования?
Да
Нет
98.С помощью какой команды может быть создан массив с именем test из десяти
элементов типа int?
int test
[10]
99. Верно ли то, что существуют такие классы, которые могут иметь один и только
один экземпляр?
Да
Нет
Как называется специализированный метод класса предназначенный для создания нового экземпляра
Тип работы: Тесты
Сдано в учебном заведении: МТИ МосТех МосАП МФПУ Синергия
Описание:
Объектно-ориентированное программирование. Синергия. Тест. 2023 год. Перед покупкой убедитесь что вопросы вам подходят.
1. Верно ли, что понятие «суперкласс» является синонимом родительского класса?
2. Верно ли, что понятия «вложенный класс» и «класс-член» являются синонимами?
3. Верно ли, что программист не может управлять сборкой мусора?
4. Верно ли, что концепция MVC включает в себя ровно три структурных составляющих: модель, вид и контроллер?
5. Верно ли, что наиболее общим отношением между классами является отношение зависимости?
6. Верно ли утверждение о том, что Java является декларативным языком
7. Верно ли утверждение о том, что Java является императивным языком программирования?
8. Верно ли, что Java позволяет создавать массив заранее известных величин?
9. Верно ли, что MVC позволяет визуализировать одни и те же данные разными способами без изменения модели?
10. Верно ли, что MVC позволяет подключать к одному виду несколько контроллеров?
11. Верно ли, что для создания деструктора в Java используется блок finalize?
12. Верно ли, что один абстрактный класс не может являться родительским по отношению к другому абстрактному классу?
13. Верно ли, что абстрактный класс не может иметь экземпляров?
14. Верно ли, что абстрактный класс не может содержать реализаций методов?
15. Верно ли, что в активной модели MVC об изменениях данных представление оповещает контроллер?
16. Верно ли, что в пассивной модели MVC об изменениях данных представление оповещает контроллер?
17. Верно ли, что основная цель использование концепции MVC – разграничение логики работы и визуализации?
18. Верно ли, что основная цель использование концепции MVC – упрощение архитектуру приложения?
19. Верно ли, что отношение агрегации может применяться для изображения иерархических отношений между классами?
20. Как расшифровывается аббревиатура JRE?
21. Как расшифровывается аббревиатура JDK?
22. Как расшифровывается аббревиатура JVM?
23. Какая из парадигм программирования является ведующей в языке Java?
24. Какая команда используется для выполнения байт-кода?
25. Какие группы шаблонов проектирования обычно выделяют?
26. Задачи какого рода решают основные шаблоны проектирования?
27. Задачи какого рода решают поведенческие шаблоны проектирования?
28. Задачи какого рода решают порождающие шаблоны проектирования?
29. Задачи какого рода решают структурные шаблоны проектирования?
30. Каким символом на диаграмме классов изображается тот факт, что атрибут имеет область видимости private?
31. Каким символом на диаграмме классов изображается тот факт, что атрибут имеет область видимости protected?
32. Каким символом на диаграмме классов изображается тот факт, что атрибут имеет бесконечную кратность?
33. Каким символом на диаграмме классов изображается тот факт, что атрибут имеет область видимости public?
34. Верно ли то, что отношение композиции является частным случаем отношения агрегации?
Программирование на языке Delphi
Глава 3. Объектно-ориентированное программирование (ООП)
Авторы: А.Н. Вальвачев
К.А. Сурков
Д.А. Сурков
Ю.М. Четырько
Опубликовано: 19.11.2005
Исправлено: 10.12.2016
Версия текста: 1.0
Объекты — это крупнейшее достижение в современной технологии программирования. Они позволили строить программу не из чудовищных по сложности процедур и функций, а из кирпичиков-объектов, заранее наделенных нужными свойствами. Самое приятное в объктах то, что их внутренняя сложность скрыта от программиста, который просто пользуется готовым строительным материалом.
Сейчас преимущества использования объектов очевидны для всех. Однако так было не всегда. Сначала старая гвардия не поняла и не приняла объекты, поэтому они почти 20 лет потихоньку развивались в различных языках, первым из которых была Simula 67. Постепенно объектно-ориентированный подход нашел себе место и в более мощных языках, таких как C++, Delphi и множестве других языков. Блестящим примером реализации объектов была библиотека Turbo Vision, предназначенная для построения пользовательского интерфейса программ в операционной системе MS-DOS.
Полную победу объекты одержали с приходом эпохи многофункциональных графических пользовательских интерфейсов. Теперь без объектов в программировании просто не обойтись. Чтобы вы не рылись в других книгах, собирая информацию по крохам, мы не поленились и объединили в этой главе все, что нужно знать об объектах. Для новичка важнейшее здесь: инкапсуляция, наследование, полиморфизм, остальное можно просто просмотреть и возвращаться к материалу по мере накопления опыта. Профессионалу полезно прочитать внимательно все от начала до конца. Поэтому давайте засучим рукава и приступим к делу.
3.1. Краеугольные камни ООП
3.1.1. Формула объекта
Авторы надеются, что читатель помнит кое-что из главы 2 и такие понятия как тип данных, процедура, функция, запись для него не в новинку. Это прекрасно. Так вот, в конце 60-х годов кому-то пришло в голову объединить эти понятия, и то, что получилось, назвать объектом. Рассмотрение данных в неразрывной связи с методами их обработки позволило вывести формулу объекта :
Объект = Данные + Операции
На основании этой формулы была разработана методология объектно-ориентированного программирования (ООП).
3.1.2. Природа объекта
Об объектах можно думать как о полезных существах, которые «живут» в вашей программе и коллективно решают некоторую прикладную задачу. Вы, как Демиург, лепите этих существ, распределяете между ними обязанности и устанавливаете правила их взаимодействия.
В общем случае каждый объект «помнит» необходимую информацию, «умеет» выполнять некоторый набор действий и характеризуется набором свойств. То, что объект «помнит», хранится в его полях . То, что объект «умеет делать», реализуется в виде его внутренних процедур и функций, называемых методами . Свойства объектов аналогичны свойствам, которые мы наблюдаем у обычных предметов. Значения свойств можно устанавливать и читать. Программно свойства реализуются через поля и методы.
Например, объект «кнопка» имеет свойство «цвет». Значение цвета кнопка запоминает в одном из своих полей. При изменении значения свойства «цвет» вызывается метод, который перерисовывает кнопку.
Кстати, этот пример позволяет сделать важный вывод: свойства имеют первостепенное значение для программиста, использующего объект. Чтобы понять суть и назначение объекта вы обязательно должны знать его свойства, иногда — методы, очень редко — поля (объект и сам знает, что с ними делать).
3.1.3. Объекты и компоненты
Когда прикладные программы были консольно-ориентированными, а пользовательский интерфейс был простым, объекты казались пределом развития программирования, поскольку были идеальным средством разбиения сложных задач на простые подзадачи. Однако с появлением графических систем программирование пользовательского интерфейса резко усложнилось. Программист в какой-то мере стал дизайнером, а визуальная компоновка и увязка элементов пользовательского интерфейса (кнопок, меток, строк редактора) начали отнимать основную часть времени. И тогда программистам пришла в голову идея визуализировать объекты, объединив программную часть объекта с его видимым представлением на экране дисплея в одно целое. То, что получилось в результате, было названо компонентом.
Компоненты в среде Delphi — это особые объекты, которые являются строительными кирпичиками визуальной среды разработки и приспособлены к визуальной установке свойств. Чтобы превратить объект в компонент, первый разрабатывается по определенным правилам, а затем помещается в палитру компонентов. Конструируя приложение, вы берете компоненты из Палитры Компонентов, располагаете на форме и устанавливаете их свойства в окне Инспектора Объектов. Внешне все выглядит просто, но чтобы достичь такой простоты, потребовалось создать механизмы, обеспечивающие функционирование объектов-компонентов уже на этапе проектирования приложения! Все это было придумано и блестяще реализовано в среде Delphi. Таким образом, компонентный подход значительно упростил создание приложений с графическим пользовательским интерфейсом и дал толчок развитию новой индустрии компонентов.
В данной главе мы рассмотрим лишь вопросы создания и использования объектов. Чуть позже мы научим вас превращать объекты в компоненты (см. главу 13).
3.1.4. Классы объектов
Каждый объект всегда принадлежит некоторому классу объектов. Класс объектов — это обобщенное (абстрактное) описание множества однотипных объектов. Объекты являются конкретными представителями своего класса, их принято называть экземплярами класса . Например, класс СОБАКИ — понятие абстрактное, а экземпляр этого класса МОЙ ПЕС БОБИК — понятие конкретное.
3.1.5. Три кита ООП
Весь мир ООП держится на трех китах: инкапсуляции, наследовании и полиморфизме. Для начала о них надо иметь только самое общее представление.
Объединение данных и операций в одну сущность — объект — тесно связано с понятием инкапсуляции, которое означает сокрытие внутреннего устройства. Инкапсуляция делает объекты похожими на маленькие программные модули, в которых скрыты внутренние данные и у которых имеется интерфейс использования в виде подпрограмм. Переход от понятий «структура данных» и «алгоритм» к понятию «объект» значительно повысил ясность и надежность программ.
Второй кит ООП — наследование . Этот простой принцип означает, что если вы хотите создать новый класс объектов, который расширяет возможности уже существующего класса, то нет необходимости в переписывании заново всех полей, методов и свойств. Вы объявляете, что новый класс является потомком (или дочерним классом ) имеющегося класса объектов, называемого предком (или родительским классом ), и добавляете к нему новые поля, методы и свойства. Процесс порождения новых классов на основе других классов называется наследованием . Новые классы объектов имеют как унаследованные признаки, так и, возможно, новые. Например, класс СОБАКИ унаследовал многие свойства своих предков — ВОЛКОВ.
Третий кит — это полиморфизм . Он означает, что в производных классах вы можете изменять работу уже существующих в базовом классе методов. При этом весь программный код, управляющий объектами родительского класса, пригоден для управления объектами дочернего класса без всякой модификации. Например, вы можете породить новый класс кнопок с рельефной надписью, переопределив метод рисования кнопки. Новую кнопку можно «подсунуть» вместо стандартной в какую-нибудь подпрограмму, вызывающую рисование кнопки. При этом подпрограмма «думает», что работает со стандартной кнопкой, но на самом деле кнопка принадлежит производному классу кнопок и отображается в новом стиле. Пока достаточно самого поверхностного понимания всех приведенных выше понятий, ниже мы рассмотрим их подробнее и покажем, как они реализованы в среде Delphi.
3.2. Классы
Для поддержки ООП в язык Delphi введены объектные типы данных , с помощью которых одновременно описываются данные и операции над ними. Объектные типы данных называют классами , а их экземпляры — объектами .
Классы объектов определяются в секции type глобального блока. Описание класса начинается с ключевого слова class и заканчивается ключевым словом end . По форме объявления классы похожи на обычные записи, но помимо полей данных могут содержать объявления пользовательских процедур и функций. Такие процедуры и функции обобщенно называют методами , они предназначены для выполнения над объектами различных операций. Приведем пример объявления класса, который предназначен для чтения текстового файла в формате «delimited text» (файл в таком формате представляет собой последовательность строк; каждая строка состоит из значений, которые отделены друг от друга символом-разделителем):
type TDelimitedReader = class// Поля FileVar: TextFile; Items: arrayofstring; Delimiter: Char; // Методыprocedure PutItem(Index: Integer; const Item: string); procedure SetActive(const AActive: Boolean); function ParseLine(const Line: string): Integer; function NextLine: Boolean; function GetEndOfFile: Boolean; end;
Класс содержит поля (FileVar, Items, Delimiter) и методы (PutItem, SetActive, ParseLine, NextLine, GetEndOfFile). Заголовки методов, (всегда) следующие за списком полей, играют роль упреждающих (forward) описаний. Программный код методов пишется отдельно от определения класса и будет приведен позже.
Класс обычно описывает сущность, моделируемую в программе. Например, класс TDelimitedReader представляет собой «читатель» текстового файла с разбором считываемых строк на элементы (подстроки), которые отделены друг от друга некоторым символом, называемым разделителем.
Класс содержит несколько полей:
- FileVar — файловая переменная, необходимая для доступа к файлу;
- Delimiter — символ, который служит разделителем элементов;
- Items — массив элементов, полученных разбором последней считанной строки;
Класс также содержит ряд методов (процедур и функций):
- PutItem — помещает элемент в массив Items по индексу Index; если индекс превышает верхнюю границу массива, то размер массива автоматически увеличивается;
- SetActive — открывает или закрывает файл, из которого производится чтение строк;
- ParseLine — осуществляет разбор строки: выделяет элементы из строки и помещает их в массив Items; возвращает количество выделенных элементов;
- NextLine — считывает очередную строку из файла и с помощью метода ParseLine осуществляет ее разбор; в случае успешного чтения очередной строки функция возвращает значение True, а иначе — значение False (достигнут конец файла);
- GetEndOfFile — возвращает булевское значение, показывающее, достигнут ли конец файла.
Обратите внимание, что приведенное выше описание является ничем иным, как декларацией интерфейса для работы с объектами класса TDelimitedReader. Реализация методов PutItem, SetActive, ParseLine, NextLine и GetEndOfFile на данный момент отсутствует, однако для создания и использования экземпляров класса она пока и не нужна.
В некотором смысле объекты похожи на программные модули, для использования которых необходимо изучить лишь интерфейсную часть, раздел реализации для этого изучать не требуется. Поэтому дальше от описания класса мы перейдем не к реализации методов, а к созданию на их основе объектов.
3.3. Объекты
Чтобы от описания класса перейти к объекту, следует выполнить соответствующее объявление в секции var :
var Reader: TDelimitedReader;
При работе с обычными типами данных этого объявления было бы достаточно для получения экземпляра типа. Однако объекты в среде Delphi являются динамическими данными, т.е. распределяются в динамической памяти. Поэтому переменная Reader — это просто ссылка на экземпляр ( объект в памяти), которого физически еще не существует. Чтобы сконструировать объект (выделить память для экземпляра) класса TDelimitedReader и связать с ним переменную Reader, нужно в тексте программы поместить следующий оператор:
Reader := TDelimitedReader.Create;
Create — это так называемый конструктор объекта; он всегда присутствует в классе и служит для создания и инициализации экземпляров. При создании объекта в памяти выделяется место только для его полей. Методы, как и обычные процедуры и функции, помещаются в область кода программы; они умеют работать с любыми экземплярами своего класса и не дублируются в памяти.
После создания объект можно использовать в программе: получать и устанавливать значения его полей, вызывать его методы. Доступ к полям и методам объекта происходит с помощью уточненных имен, например:
Reader.NextLine;
Кроме того, как и при работе с записями, допустимо использование оператора with , например:
with Reader do NextLine;
Если объект становится ненужным, он должен быть удален вызовом специального метода Destroy, например:
Reader.Destroy; // Освобождение памяти, занимаемой объектом
Destroy — это так называемый деструктор объекта; он присутствует в классе наряду с конструктором и служит для удаления объекта из динамической памяти. После вызова деструктора переменная Reader становится несвязанной и не должна использоваться для доступа к полям и методам уже несуществующего объекта. Чтобы отличать в программе связанные объектные переменные от несвязанных, последние следует инициализировать значением nil . Например, в следующем фрагменте обращение к деструктору Destroy выполняется только в том случае, если объект реально существует:
Reader := nil; . if Reader <> nilthen Reader.Destroy;
Вызов деструктора для несуществующих объектов недопустим и при выполнении программы приведет к ошибке. Чтобы избавить программистов от лишних ошибок, в объекты ввели предопределенный метод Free, который следует вызывать вместо деструктора. Метод Free сам вызывает деструктор Destroy, но только в том случае, если значение объектной переменной не равно nil . Поэтому последнюю строчку в приведенном выше примере можно переписать следующим образом.
Reader.Free;
После уничтожения объекта переменная Reader сохраняет свое значение, продолжая ссылаться на место в памяти, где объекта уже нет. Если эту переменную предполагается еще использовать, то желательно присвоить ей значение nil , чтобы программа могла проверить, существует объект или нет. Таким образом, наиболее правильная последовательность действий при уничтожении объекта должна быть следующая:
Reader.Free; Reader := nil;
С помощью стандартной процедуры FreeAndNil это можно сделать проще и элегантнее:
FreeAndNil(Reader); // Эквивалентно: Reader.Free; Reader := nil;
Значение одной объектной переменной можно присвоить другой. При этом объект не копируется в памяти, а вторая переменная просто связывается с тем же объектом, что и первая:
var R1, R2: TDelimitedReader; // Переменные R1 и R2 не связаны с объектомbegin R1 := TDelimitedReader.Create; // Связывание переменной R1 с новым объектом// Переменная R2 пока еще не связана ни с каким объектом R2 := R1; // Связывание переменной R2 с тем же объектом, что и R1 // Теперь обе переменные связаны с одним объектом R2.Free; // Уничтожение объекта// Теперь R1 и R2 не связаны ни с каким объектом end;
Объекты могут выступать в программе не только в качестве переменных, но также элементов массивов, полей записей, параметров процедур и функций. Кроме того, они могут служить полями других объектов. Во всех этих случаях программист фактически оперирует указателями на экземпляры объектов в динамической памяти. Следовательно, объекты изначально приспособлены для создания сложных динамических структур данных, таких как списки и деревья. Указатели на объекты для этого не нужны.
В некоторых случаях требуется, чтобы объекты разных классов содержали ссылки друг на друга. Возникает проблема: объявление первого класса будет содержать ссылку на еще не определенный класс. Она решается с помощью упреждающего объявления:
type TReadersList = class; // упреждающее объявление класса TReadersList TDelimitedReader = class Owner: TReadersList; . end; TReadersList = class Readers: arrayof TDelimitedReader; . end;
Первое объявление класса TDelimitedReader называется упреждающим (от англ. forward). Оно необходимо для того, чтобы компилятор нормально воспринял объявление поля Owner в классе TDelimitedReader.
Итак, вы уже имеете некоторое представление об объектах, перейдем теперь к вопросу реализации их методов.
3.4. Конструкторы и деструкторы
Особой разновидностью методов являются конструкторы и деструкторы . Напомним, что конструкторы создают, а деструкторы разрушают объекты. Создание объекта включает выделение памяти под экземпляр и инициализацию его полей, а разрушение — очистку полей и освобождение памяти. Действия по инициализации и очистке полей специфичны для каждого конкретного класса объектов. По этой причине язык Delphi позволяет переопределить стандартный конструктор Create и стандартный деструктор Destroy для выполнения любых полезных действий. Можно даже определить несколько конструкторов и деструкторов (имена им назначает сам программист), чтобы обеспечить различные процедуры создания и разрушения объектов.
Объявление конструкторов и деструкторов похоже на объявление обычных методов с той лишь разницей, что вместо зарезервированных слов function и procedure используются слова constructor и destructor . Для нашего класса TDelimitedReader потребуется конструктор, которому в качестве параметра будет передаваться имя обрабатываемого файла и разделитель элементов:
type TDelimitedReader = class . // Конструкторы и деструкторыconstructor Create(const FileName: string; const ADelimiter: Char = ';'); destructor Destroy; override; . end;
Приведем их возможную реализацию:
constructor TDelimitedReader.Create(const FileName: string; const ADelimiter: Char = ';'); begin AssignFile(FileVar, FileName); Delimiter := ADelimiter; end; destructor TDelimitedReader.Destroy; begin// Пока ничего не делаемend;
Если объект содержит встроенные объекты или другие динамические данные, то конструктор — это как раз то место, где их нужно создавать.
Конструктор применяется к классу или к объекту. Если он применяется к классу,
Reader := TDelimitedReader.Create('MyData.del', ';');
то выполняется следующая последовательность действий:
- в динамической памяти выделяется место для нового объекта;
- выделенная память заполняется нулями. В результате все числовые поля и поля порядкового типа приобретают нулевые значения, строковые поля становятся пустыми, а поля, содержащие указатели и объекты получают значение nil ;
- затем выполняются заданные программистом действия конструктора;
- ссылка на созданный объект возвращается в качестве значения конструктора. Тип возвращаемого значения совпадает с типом класса, использованного при вызове (в нашем примере это тип TDelimitedReader).
Если конструктор применяется к объекту,
Reader.Create('MyData.del', ';');
то конструктор выполняется как обычный метод. Другими словами, новый объект не создается, а происходит повторная инициализация полей существующего объекта. В этом случае конструктор не возвращает никакого значения. Далеко не все объекты корректно себя ведут при повторной инициализации, поскольку программисты редко закладывают такую возможность в свои классы. Поэтому на практике повторная инициализация применяется крайне редко.
Деструктор уничтожает объект, к которому применяется:
Reader.Destroy;
- выполняется заданный программистом код завершения;
- освобождается занимаемая объектом динамическая память.
В теле деструктора обычно должны уничтожаться встроенные объекты и динамические данные, как правило, созданные конструктором.
Как и обычные методы, деструктор может иметь параметры, но эта возможность используется редко.
3.5. Методы
Процедуры и функции, предназначенные для выполнения над объектами действий, называются методами . Предварительное объявление методов выполняется при описании класса в секции interface модуля, а их программный код записывается в секции implementation . Однако в отличие от обычных процедур и функций заголовки методов должны иметь уточненные имена, т.е. содержать наименование класса. Приведем возможную реализацию одного из методов в классе TDelimitedReader:
procedure TDelimitedReader.SetActive(const AActive: Boolean); beginif AActive then Reset(FileVar) // Открытие файлаelse CloseFile(FileVar); // Закрытие файлаend;
Обратите внимание, что внутри методов обращения к полям и другим методам выполняются как к обычным переменным и подпрограммам без уточнения экземпляра объекта. Такое упрощение достигается путем использования в пределах метода псевдопеременной Self (стандартный идентификатор). Физически Self представляет собой дополнительный неявный параметр, передаваемый в метод при вызове. Этот параметр и указывает экземпляр объекта, к которому данный метод применяется. Чтобы пояснить сказанное, перепишем метод SetActive, представив его в виде обычной процедуры:
procedure TDelimitedReader_SetActive(Self: TDelimitedReader; const AActive: Boolean); beginif AActive then Reset(Self.FileVar) // Открытие файлаelse CloseFile(Self.FileVar); // Закрытие файлаend;
Согласитесь, что метод SetActive выглядит лаконичнее процедуры TDelimitedReader_SetActive.
Практика показывает, что псевдопеременная Self редко используется в явном виде. Ее необходимо применять только тогда, когда при написании метода может возникнуть какая-либо двусмысленность для компилятора, например при использовании одинаковых имен и для локальных переменных, и для полей объекта.
Если выполнить метод SetActive,
Reader.SetActive(True);
то обрабатываемый файл будет открыт. При этом неявный параметр Self будет содержать значение переменной Reader. Такой вызов реализуется обычными средствами процедурного программирования приблизительно так:
TDelimitedReader_SetActive(Reader, True);
3.6. Свойства
3.6.1. Понятие свойства
Помимо полей и методов в объектах существуют свойства . При работе с объектом свойства выглядят как поля: они принимают значения и участвуют в выражениях. Но в отличие от полей свойства не занимают места в памяти, а операции их чтения и записи ассоциируются с обычными полями или методами. Это позволяет создавать необходимые сопутствующие эффекты при обращении к свойствам. Например, в объекте Reader присваивание свойству Active значения True вызовет открытие файла, а присваивание значения False — закрытие файла. Создание сопутствующего эффекта (открытие или закрытие файла) достигается тем, что за присваиванием свойству значения стоит вызов метода.
Объявление свойства выполняется с помощью зарезервированного слова property , например:
type TDelimitedReader = class . FActive: Boolean; . // Метод записи (установки значения) свойстваprocedure SetActive(const AActive: Boolean); property Active: Boolean read FActive write SetActive; // Свойствоend;
Ключевые слова read и write называются спецификаторами доступа. После слова read указывается поле или метод, к которому происходит обращение при чтении (получении) значения свойства, а после слова write — поле или метод, к которому происходит обращение при записи (установке) значения свойства. Например, чтение свойства Active означает чтение поля FActive, а установка свойства — вызов метода SetActive. Чтобы имена свойств не совпадали с именами полей, последние принято писать с буквы F (от англ. field). Мы в дальнейшем также будем пользоваться этим соглашением. Начнем с того, что переименуем поля класса TDelimitedReader: поле FileVar переименуем в FFile, Items — в FItems, а поле Delimiter — в FDelimiter.
type TDelimitedReader = class// Поля FFile: TextFile; // FileVar -> FFile FItems: arrayofstring; // Items -> FItems FActive: Boolean; FDelimiter: Char; // Delimiter -> FDelimiter . end;
Обращение к свойствам выглядит в программе как обращение к полям:
var Reader: TDelimitedReader; IsOpen: Boolean; . Reader.Active := True; // Эквивалентно Reader.SetActive(True); IsOpen := Reader.Active; // Эквивалентно IsOpen := Reader.FActive
Если один из спецификаторов доступа опущен, то значение свойства можно либо только читать (задан спецификатор read ), либо только записывать (задан спецификатор write ). В следующем примере объявлено свойство, значение которого можно только читать.
type TDelimitedReader = class . FItems: arrayofstring; . function GetItemCount: Integer; . property ItemCount: Integer read GetItemCount; // Только для чтения!end; function TDelimitedReader.GetItemCount: Integer; begin Result := Length(FItems); end;
Здесь свойство ItemCount показывает количество элементов в массиве FItems. Поскольку оно определяется в результате чтения и разбора очередной строки файла, пользователю объекта разрешено лишь узнавать количество элементов.
В отличие от полей свойства не имеют адреса в памяти, поэтому к ним запрещено применять операцию @. Как следствие, их нельзя передавать в var — и out -параметрах процедур и функций.
Технология объектно-ориентированного программирования в среде Delphi предписывает избегать прямого обращения к полям, создавая вместо этого соответствующие свойства. Это упорядочивает работу с объектами, изолируя их данные от непосредственной модификации. В будущем внутренняя структура класса, которая иногда является достаточно сложной, может быть изменена с целью повышения эффективности работы программы. При этом потребуется переработать только методы чтения и записи значений свойств; внешний интерфейс класса не изменится.
3.6.2. Методы получения и установки значений свойств
Методы получения (чтения) и установки (записи) значений свойств подчиняются определенным правилам. Метод чтения свойства — это всегда функция, возвращающая значение того же типа, что и тип свойства. Метод записи свойства — это обязательно процедура, принимающая параметр того же типа, что и тип свойства. В остальных отношениях это обычные методы объекта. Примерами методов чтения и записи свойств являются методы GetItemCount и SetActive в классе TDelimitedReader:
type TDelimitedReader = class FActive: Boolean; . procedure SetActive(const AActive: Boolean); function GetItemCount: Integer; . property Active: Boolean read FActive write SetActive; property ItemCount: Integer read GetItemCount; end;
Использование методов для получения и установки свойств позволяет проверить корректность значения свойства, сделать дополнительные вычисления, установить значения зависимых полей и т.д. Например, в методе SetActive вполне целесообразно осуществить проверку состояния файла (открыт или закрыт), чтобы избежать его повторного открытия или закрытия:
procedure TDelimitedReader.SetActive(const AActive: Boolean); beginif Active <> AActive then// Если состояние изменяетсяbeginif AActive then Reset(FFile) // Открытие файлаelse CloseFile(FFile); // Закрытие файла FActive := AActive; // Сохранение состояния в полеend; end;
Наличие свойства Active позволяет нам отказаться от использования методов Open и Close, традиционных при работе с файлами. Согласитесь, что открывать и закрывать файл с помощью свойства Active гораздо удобнее и естественнее. Одновременно с этим свойство Active можно использовать и для проверки состояния файла (открыт или нет). Таким образом, для осуществления трех действий требуется всего лишь одно свойство! Это делает использование Ваших классов другими программистами более простым, поскольку им легче запомнить одно понятие Active, чем, например, три метода: Open, Close и IsOpen.
Значение свойства может не храниться, а вычисляться при каждом обращении к свойству. Примером является свойство ItemCount, значение которого вычисляется как Length(FItems).
3.6.3. Свойства-массивы
Кроме обычных свойств в объектах существуют свойства-массивы (array properties). Свойство-массив — это индексированное множество значений. Например, в классе TDelimitedReader множество элементов, выделенных из считанной строки, удобно представить в виде свойства-массива:
type TDelimitedReader = class . FItems: arrayofstring; . function GetItem(Index: Integer): string; . property Items[Index: Integer]: stringread GetItem; end; function TDelimitedReader.GetItem(Index: Integer): string; begin Result := FItems[Index]; end;
Элементы массива Items можно только читать, поскольку класс TDelimitedReader предназначен только для чтения данных из файла.
В описании свойства-массива разрешено использовать только методы, но не поля. В этом состоит отличие свойства-массива от обычного свойства.
Основная выгода от применения свойства-массива — возможность выполнения итераций с помощью цикла for , например:
var Reader: TDelimitedReader; I: Integer; . for I := 0 to Reader.ItemCount - 1 do Writeln(Reader.Items[I]); .
Свойство-массив может быть многомерным. В этом случае методы чтения и записи элементов должны иметь столько же индексных параметров соответствующих типов, что и свойство-массив.
Свойства-массивы имеют два важных отличия от обычных массивов:
их индексы не ограничиваются диапазоном и могут иметь любой тип данных, а не только Integer. Например, можно создать свойство-массив, в котором индексами будут строки. Обращение к такому свойству могло бы выглядеть примерно так:
Reader.Items['FirstName'] := 'Alexander';
операции над свойством-массивом в целом запрещены; разрешены операции только с его элементами.
3.6.4. Свойство-массив как основное свойство объекта
Свойство-массив можно сделать основным свойством объектов данного класса. Для этого в описание свойства добавляется слово default :
type TDelimitedReader = class . property Items[Index: Integer]: stringread GetItem; default; . end;
Такое объявление свойства Items позволяет рассматривать сам объект класса TDelimitedReader как массив и опускать имя свойства-массива при обращении к нему из программы, например:
var R: TDelimitedReader; I: Integer; . for I := 0 to R.ItemCount - 1 do Writeln(R[I]); .
Следует помнить, что только свойства-массивы могут быть основными свойствами объектов; для обычных свойств это недопустимо.
3.6.5. Методы, обслуживающие несколько свойств
Один и тот же метод может использоваться для получения (установки) значений нескольких свойств одного типа. В этом случае каждому свойству назначается целочисленный индекс, который передается в метод чтения (записи) первым параметром.
В следующем примере уже известный Вам метод GetItem обслуживает три свойства: FirstName, LastName и Phone:
type TDelimitedReader = class . property FirstName: stringindex 0 read GetItem; property LastName: stringindex 1 read GetItem; property Phone: stringindex 2 read GetItem; end;
Обращения к свойствам FirstName, LastName и Phone заменяются компилятором на вызовы одного и того же метода GetItem, но с разными значениями параметра Index:
var Reader: TDelimitedReader; . Writeln(Reader.FirstName); // Эквивалентно: Writeln(Reader.GetItem(0)); Writeln(Reader.LastName); // Эквивалентно: Writeln(Reader.GetItem(1)); Writeln(Reader.Phone); // Эквивалентно: Writeln(Reader.GetItem(2)); .
Обратите внимание, что метод GetItem обслуживает как свойство-массив Items, так и свойства FirstName, LastName и Phone. Удобно, не правда ли!
Перед тем, как перейти к более сложным понятиям ООП, приведем полную реализацию класса TDelimitedReader. Настоятельно рекомендуем Вам внимательно ознакомиться с этой реализацией, поскольку в ней сведено воедино все то, о чем говорилось в предыдущих разделах.
type TDelimitedReader = class// Поля FFile: TextFile; FItems: arrayofstring; FActive: Boolean; FDelimiter: Char; // Методы чтения и записи свойствprocedure SetActive(const AActive: Boolean); function GetItemCount: Integer; function GetEndOfFile: Boolean; function GetItem(Index: Integer): string; // Методыprocedure PutItem(Index: Integer; const Item: string); function ParseLine(const Line: string): Integer; function NextLine: Boolean; // Конструкторы и деструкторыconstructor Create(const FileName: string; const ADelimiter: Char = ';'); destructor Destroy; override; // Свойстваproperty Active: Boolean read FActive write SetActive; property Items[Index: Integer]: stringread GetItem; default; property ItemCount: Integer read GetItemCount; property EndOfFile: Boolean read GetEndOfFile; property Delimiter: Char read FDelimiter; end; constructor TDelimitedReader.Create(const FileName: string; const ADelimiter: Char = ';'); begin AssignFile(FFile, FileName); FActive := False; FDelimiter := ADelimiter; end; destructor TDelimitedReader.Destroy; begin Active := False; end; function TDelimitedReader.GetEndOfFile: Boolean; begin Result := Eof(FFile); end; function TDelimitedReader.GetItem(Index: Integer): string; begin Result := FItems[Index]; end; function TDelimitedReader.GetItemCount: Integer; begin Result := Length(FItems); end; function TDelimitedReader.NextLine: Boolean; var S: string; N: Integer; begin Result := not EndOfFile; if Result then// Если не достигнут конец файлаbegin Readln(FFile, S); // Чтение очередной строки из файла N := ParseLine(S); // Разбор считанной строкиif N <> ItemCount then SetLength(FItems, N); // Отсечение массива (если необходимо)end; end; function TDelimitedReader.ParseLine(const Line: string): Integer; var S: string; P: Integer; begin S := Line; Result := 0; repeat P := Pos(Delimiter, S); // Поиск разделителяif P = 0 then// Если разделитель не найден, то считается, что P := Length(S) + 1; // разделитель находится за последним символом PutItem(Result, Copy(S, 1, P - 1)); // Установка элемента Delete(S, 1, P); // Удаление элемента из строки Result := Result + 1; // Переход к следующему элементуuntil S = ''; // Пока в строке есть символыend; procedure TDelimitedReader.PutItem(Index: Integer; const Item: string); beginif Index > High(FItems) then// Если индекс выходит за границы массива, SetLength(FItems, Index + 1); // то увеличение размера массива FItems[Index] := Item; // Установка соответствующего элементаend; procedure TDelimitedReader.SetActive(const AActive: Boolean); beginif Active <> AActive then// Если состояние изменяетсяbeginif AActive then Reset(FFile) // Открытие файлаelse CloseFile(FFile); // Закрытие файла FActive := AActive; // Сохранение состояния в полеend; end;
3.7. Наследование
3.7.1. Понятие наследования
Классы инкапсулируют (т.е. включают в себя) поля, методы и свойства; это их первая черта. Следующая не менее важная черта классов — способность наследовать поля, методы и свойства других классов. Чтобы пояснить сущность наследования обратимся к примеру с читателем текстовых файлов в формате «delimited text».
Класс TDelimitedReader описывает объекты для чтения из текстового файла элементов, разделенных некоторым символом. Он не пригоден для чтения элементов, хранящихся в другом формате, например в формате с фиксированным количеством символов для каждого элемента. Для этого необходим другой класс:
type TFixedReader = classprivate// Поля FFile: TextFile; FItems: arrayofstring; FActive: Boolean; FItemWidths: arrayof Integer; // Методы чтения и записи свойствprocedure SetActive(const AActive: Boolean); function GetItemCount: Integer; function GetEndOfFile: Boolean; function GetItem(Index: Integer): string; // Методыprocedure PutItem(Index: Integer; const Item: string); function ParseLine(const Line: string): Integer; function NextLine: Boolean; // Конструкторы и деструкторыconstructor Create(const FileName: string; const AItemWidths: arrayof Integer); destructor Destroy; override; // Свойстваproperty Active: Boolean read FActive write SetActive; property Items[Index: Integer]: stringread GetItem; default; property ItemCount: Integer read GetItemCount; property EndOfFile: Boolean read GetEndOfFile; end; constructor TFixedReader.Create(const FileName: string; const AItemWidths: arrayof Integer); var I: Integer; begin AssignFile(FFile, FileName); FActive := False; // Копирование AItemWidths в FItemWidths SetLength(FItemWidths, Length(AItemWidths)); for I := 0 to High(AItemWidths) do FItemWidths[I] := AItemWidths[I]; end; destructor TFixedReader.Destroy; begin Active := False; end; function TFixedReader.GetEndOfFile: Boolean; begin Result := Eof(FFile); end; function TFixedReader.GetItem(Index: Integer): string; begin Result := FItems[Index]; end; function TFixedReader.GetItemCount: Integer; begin Result := Length(FItems); end; function TFixedReader.NextLine: Boolean; var S: string; N: Integer; begin Result := not EndOfFile; if Result then// Если не достигнут конец файлаbegin Readln(FFile, S); // Чтение очередной строки из файла N := ParseLine(S); // Разбор считанной строкиif N <> ItemCount then SetLength(FItems, N); // Отсечение массива (если необходимо)end; end; function TFixedReader.ParseLine(const Line: string): Integer; var I, P: Integer; begin P := 1; for I := 0 to High(FItemWidths) dobegin PutItem(I, Copy(Line, P, FItemWidths[I])); // Установка элемента P := P + FItemWidths[I]; // Переход к следующему элементуend; Result := Length(FItemWidths); // Количество элементов постоянноend; procedure TFixedReader.PutItem(Index: Integer; const Item: string); beginif Index > High(FItems) then// Если индекс выходит за границы массива, SetLength(FItems, Index + 1); // то увеличение размера массива FItems[Index] := Item; // Установка соответствующего элементаend; procedure TFixedReader.SetActive(const AActive: Boolean); beginif Active <> AActive then// Если состояние изменяетсяbeginif AActive then Reset(FFile) // Открытие файлаelse CloseFile(FFile); // Закрытие файла FActive := AActive; // Сохранение состояния в полеend; end;
Поля, свойства и методы класса TFixedReader практически полностью аналогичны тем, что определены в классе TDelimitedReader. Отличие состоит в отсутствии свойства Delimiter, наличии поля FItemWidths (для хранения размеров элементов), другой реализации метода ParseLine и немного отличающемся конструкторе. Если в будущем появится класс для чтения элементов из файла еще одного формата (например, зашифрованного текста), то придется снова определять общие для всех классов поля, методы и свойства. Чтобы избавиться от дублирования общих атрибутов (полей, свойств и методов) при определении новых классов, воспользуемся механизмом наследования. Прежде всего, выделим в отдельный класс TTextReader общие атрибуты всех классов, предназначенных для чтения элементов из текстовых файлов. Реализация методов TTextReader, кроме метода ParseLine, полностью идентична реализации TDelimitedReader, приведенной в предыдущем разделе.
type TTextReader = classprivate// Поля FFile: TextFile; FItems: arrayofstring; FActive: Boolean; // Методы получения и установки значений свойствprocedure SetActive(const AActive: Boolean); function GetItemCount: Integer; function GetItem(Index: Integer): string; function GetEndOfFile: Boolean; // Методыprocedure PutItem(Index: Integer; const Item: string); function ParseLine(const Line: string): Integer; function NextLine: Boolean; // Конструкторы и деструкторыconstructor Create(const FileName: string); destructor Destroy; override; // Свойстваproperty Active: Boolean read FActive write SetActive; property Items[Index: Integer]: string read GetItem; default; property ItemCount: Integer read GetItemCount; property EndOfFile: Boolean read GetEndOfFile; end; . constructor TTextReader.Create(const FileName: string); begin AssignFile(FFile, FileName); FActive := False; end; function TTextReader.ParseLine(const Line: string): Integer; begin// Функция просто возвращает 0, поскольку не известно,// в каком именно формате хранятся элементы Result := 0; end; .
При реализации класса TTextReader ничего не известно о том, как хранятся элементы в считываемых строках, поэтому метод ParseLine ничего не делает. Очевидно, что создавать объекты класса TTextReader не имеет смысла. Для чего тогда нужен класс TTextReader? Ответ: чтобы на его основе определить ( породить ) два других класса — TDelimitedReader и TFixedReader, предназначенных для чтения данных в конкретных форматах:
type TDelimitedReader = class(TTextReader) FDelimiter: Char; function ParseLine(const Line: string): Integer; override; constructor Create(const FileName: string; const ADelimiter: Char = ';'); property Delimiter: Char read FDelimiter; end; TFixedReader = class(TTextReader) FItemWidths: arrayof Integer; function ParseLine(const Line: string): Integer; override; constructor Create(const FileName: string; const AItemWidths: arrayof Integer); end; .
Классы TDelimitedReader и TFixedReader определены как наследники TTextReader (об этом говорит имя в скобках после слова class ). Они автоматически включают в себя все описания, сделанные в классе TTextReader и добавляют к ним некоторые новые. В результате формируется дерево классов , показанное на рисунке 3.1 (оно всегда рисуется перевернутым).
Рисунок 3.1. Дерево классов
Класс, который наследует атрибуты другого класса, называется порожденным классом или потомком . Соответственно класс, от которого происходит наследование, выступает в роли базового, или предка . В нашем примере класс TDelimitedReader является прямым потомком класса TTextReader. Если от TDelimitedReader породить новый класс, то он тоже будет потомком класса TTextReader, но уже не прямым.
Очень важно, что в отношениях наследования любой класс может иметь только одного непосредственного предка и сколь угодно много потомков. Поэтому все связанные отношением наследования классы образуют иерархию . Примером иерархии классов является библиотека VCL; с ее помощью в среде Delphi обеспечивается разработка GUI-приложений.
3.7.2. Прародитель всех классов
В языке Delphi существует предопределенный класс TObject, который служит неявным предком тех классов, для которых предок не указан. Это означает, что объявление
type TTextReader = class . end;
type TTextReader = class(TObject) . end;
Класс TObject выступает корнем любой иерархии классов. Он содержит ряд методов, которые по наследству передаются всем остальным классам. Среди них конструктор Create, деструктор Destroy, метод Free и некоторые другие методы.
Таким образом, полное дерево классов для чтения элементов из текстового файла в различных форматах выглядит так, как показано на рисунке 3.2.
Рисунок 3.2. Полное дерево классов
Поскольку класс TObject является предком для всех других классов (в том числе и для ваших собственных), то не лишним будет кратко ознакомиться с его методами:
type TObject = classconstructor Create; procedure Free; classfunction InitInstance(Instance: Pointer): TObject; procedure CleanupInstance; function ClassType: TClass; classfunction ClassName: ShortString; classfunction ClassNameIs(const Name: string): Boolean; classfunction ClassParent: TClass; classfunction ClassInfo: Pointer; classfunction InstanceSize: Longint; classfunction InheritsFrom(AClass: TClass): Boolean; classfunction MethodAddress(const Name: ShortString): Pointer; classfunction MethodName(Address: Pointer): ShortString; function FieldAddress(const Name: ShortString): Pointer; function GetInterface(const IID: TGUID; out Obj): Boolean; classfunction GetInterfaceEntry(const IID: TGUID): PInterfaceEntry; classfunction GetInterfaceTable: PInterfaceTable; function SafeCallException(ExceptObject: TObject; ExceptAddr: Pointer): HResult; virtual; procedure AfterConstruction; virtual; procedure BeforeDestruction; virtual; procedure Dispatch(var Message); virtual; procedure DefaultHandler(var Message); virtual; classfunction NewInstance: TObject; virtual; procedure FreeInstance; virtual; destructor Destroy; virtual; end;
Некоторые конструкции этого описания будут вам непонятны, поскольку мы их еще не изучали. Сейчас это не важно. Снова вернитесь к этому описанию после прочтения всей главы.
Краткое описание методов в классе TObject:
- Create — стандартный конструктор.
- Free — уничтожает объект: вызывает стандартный деструктор Destroy, если значение псевдопеременной Self не равно nil .
- InitInstance (Instance: Pointer): TObject — при создании объекта инициализирует нулями выделенную память. На практике нет необходимости вызывать этот метод явно.
- CleanupInstance — освобождает память, занимаемую полями с типом string, Variant, динамический массив и интерфейс. На практике нет необходимости вызывать этот метод явно.
- ClassType : TClass — возвращает описатель класса (метакласс).
- ClassName : ShortString — возвращает имя класса.
- ClassNameIs (const Name: string): Boolean — проверяет, является ли заданная строка именем класса.
- ClassParent : TClass — возвращает описатель базового класса.
- ClassInfo : Pointer — возвращает указатель на соответствующую классу таблицу RTTI (от англ. Runtime Type Information). Таблица RTTI используется для проверки типов данных на этапе выполнения программы.
- InstanceSize : Longint — возвращает количество байт, необходимых для хранения в памяти одного объекта соответствующего класса. Заметим, что значение, возвращаемое этим методом и значение, возвращаемое функцией SizeOf при передаче ей в качестве аргумента объектной переменной — это разные значения. Функция SizeOf всегда возвращает значение 4 (SizeOf(Pointer)), поскольку объектная переменная — это ни что иное, как ссылка на данные объекта в памяти. Значение InstanceSize — это размер этих данных, а не размер объектной переменной.
- InheritsFrom (AClass: TClass): Boolean — проверяет, является ли класс AClass базовым классом.
- MethodAddress (const Name: ShortString): Pointer — возвращает адрес published-метода, имя которого задается параметром Name.
- MethodName (Address: Pointer): ShortString — возвращает имя published-метода по заданному адресу.
- FieldAddress (const Name: ShortString): Pointer — возвращает адрес published-поля, имя которого задается параметром Name.
- GetInterface (const IID: TGUID; out Obj): Boolean — возвращает ссылку на интерфейс через параметр Obj; идентификатор интерфейса задается параметром IID. (Интерфейсы рассмотрены в главе 6)
- GetInterfaceEntry (const IID: TGUID): PInterfaceEntry — возвращает информацию об интерфейсе, который реализуется классом. Идентификатор интерфейса задается параметром IID.
- GetInterfaceTable : PInterfaceTable — возвращает указатель на таблицу с информацией обо всех интерфейсах, реализуемых классом.
- AfterConstruction — автоматически вызывается после создания объекта. Метод не предназначен для явного вызова из программы. Используется для того, чтобы выполнить определенные действия уже после создания объекта (для этого его необходимо переопределить в производных классах).
- BeforeDestruction — автоматически вызывается перед уничтожением объекта. Метод не предназначен для явного вызова из программы. Используется для того, чтобы выполнить определенные действия непосредственно перед уничтожением объекта (для этого его необходимо переопределить в производных классах).
- Dispatch (var Message) — служит для вызова методов, объявленных с ключевым словом message .
- DefaultHandler (var Message) — вызывается методом Dispatch в том случае, если метод, соответствующий сообщению Message, не был найден.
- NewInstance : TObject — вызывается при создании объекта для выделения динамической памяти, чтобы разместить в ней данные объекта. Метод вызывается автоматически, поэтому нет необходимости вызывать его явно.
- FreeInstance — вызывается при уничтожении объекта для освобождения занятой объектом динамической памяти. Метод вызывается автоматически, поэтому нет необходимости вызывать его явно.
- Destroy — стандартный деструктор.
3.7.3. Перекрытие атрибутов в наследниках
В механизме наследования можно условно выделить три основных момента:
- наследование полей;
- наследование свойств;
- наследование методов.
Любой порожденный класс наследует от родительского все поля данных, поэтому классы TDelimitedReader и TFixedReader автоматически содержат поля FFile, FActive и FItems, объявленные в классе TTextReader. Доступ к полям предка осуществляется по имени, как если бы они были определены в потомке. В потомках можно определять новые поля, но их имена должны отличаться от имен полей предка.
Наследование свойств и методов имеет свои особенности.
Свойство базового класса можно перекрыть (от англ. override) в производном классе, например чтобы добавить ему новый атрибут доступа или связать с другим полем или методом.
Метод базового класса тоже можно перекрыть в производном классе, например чтобы изменить логику его работы. Обратимся к классам TDelimitedReader и TFixedReader. В них методы PutItem, GetItem, SetActive и GetEndOfFile унаследованы от TTextReader, поскольку логика их работы не зависит от того, в каком формате хранятся данные в файле. А вот метод ParseLine перекрыт, так как способ разбора строк зависит от формата данных:
function TDelimitedReader.ParseLine(const Line: string): Integer; var S: string; P: Integer; begin S := Line; Result := 0; repeat P := Pos(Delimiter, S); // Поиск разделителяif P = 0 then// Если разделитель не найден, то считается, что P := Length(S) + 1; // разделитель находится за последним символом PutItem(Result, Copy(S, 1, P - 1)); // Установка элемента Delete(S, 1, P); // Удаление элемента из строки Result := Result + 1; // Переход к следующему элементуuntil S = ''; // Пока в строке есть символыend; function TFixedReader.ParseLine(const Line: string): Integer; var I, P: Integer; begin P := 1; for I := 0 to High(FItemWidths) dobegin PutItem(I, Copy(Line, P, FItemWidths[I])); // Установка элемента P := P + FItemWidths[I]; // Переход к следующему элементуend; Result := Length(FItemWidths); // Количество элементов постоянноend;
В классах TDelimitedReader и TFixedReader перекрыт еще и конструктор Create. Это необходимо для инициализации специфических полей этих классов (поля FDelimiter в классе TDelimitedReader и поля FItemWidths в классе TFixedReader):
constructor TDelimitedReader.Create(const FileName: string; const ADelimiter: Char = ';'); begininherited Create(FileName); FDelimiter := ADelimiter; end; constructor TFixedReader.Create(const FileName: string; const AItemWidths: arrayof Integer); var I: Integer; begininherited Create(FileName); // Копирование AItemWidths в FItemWidths SetLength(FItemWidths, Length(AItemWidths)); for I := 0 to High(AItemWidths) do FItemWidths[I] := AItemWidths[I]; end;
Как видно из примера, в наследнике можно вызвать перекрытый метод предка, указав перед именем метода зарезервированное слово inherited . Когда метод предка полностью совпадает с методом потомка по формату заголовка, то можно использовать более короткую запись. Воспользуемся ей и перепишем деструктор в классе TTextReader правильно:
destructor TTextReader.Destroy; begin Active := False; inherited; // Эквивалентно: inherited Destroy;end;
Два последних примера демонстрируют важный принцип реализации конструкторов и деструкторов. В конструкторах сначала вызывается конструктор предка, а затем инициализируются дополнительные поля данных. В деструкторах применяется обратная последовательность действий: сначала разрушаются данные, недоступные предку, а затем вызывается унаследованный деструктор. Всегда пользуйтесь этими правилами в своих программах, чтобы избежать ошибок.
3.7.4. Совместимость объектов различных классов
Для классов, связанных отношением наследования, вводится новое правило совместимости типов. Вместо объекта базового класса можно подставить объект любого производного класса. Обратное неверно. Например, переменной типа TTextReader можно присвоить значение переменной типа TDelimitedReader:
var Reader: TTextReader; . Reader := TDelimitedReader.Create('MyData.del', ';');
Объектная переменная Reader формально имеет тип TTextReader, а фактически связана с экземпляром класса TDelimitedReader.
Правило совместимости классов чаще всего применяется при передаче объектов в параметрах процедур и функций. Например, если процедура работает с объектом класса TTextReader, то вместо него можно передать объект класса TDelimitedReader или TFixedReader.
Заметим, что все объекты являются представителями известного вам класса TObject. Поэтому любой объект любого класса можно использовать как объект класса TObject.
3.7.5. Контроль и преобразование типов
Поскольку реальный экземпляр объекта может оказаться наследником класса, указанного при описании объектной переменной или параметра, бывает необходимо проверить, к какому классу принадлежит объект на самом деле. Чтобы программист мог выполнять такого рода проверки, каждый объект хранит информацию о своем классе. В языке Delphi существуют операторы is и as , с помощью которых выполняется соответственно проверка на тип (type checking) и преобразование к типу (type casting).
Например, чтобы выяснить, принадлежит ли некоторый объект Obj к классу TTextReader или его наследнику, следует использовать оператор is :
var Obj: TObject; . if Obj is TTextReader then .
Для преобразования объекта к нужному типу используется оператор as , например
with Obj as TTextReader do Active := False;
Стоит отметить, что для объектов применим и обычный способ приведения типа:
with TTextReader(Obj) do Active := False;
Вариант с оператором as лучше, поскольку безопасен. Он генерирует ошибку (точнее исключительную ситуацию; об исключительных ситуациях мы расскажем в главе 4) при выполнении программы (run-time error), если реальный экземпляр объекта Obj не совместим с классом TTextReader. Забегая вперед, скажем, что ошибку приведения типа можно обработать и таким образом избежать досрочного завершения программы.
3.8. Виртуальные методы
3.8.1. Понятие виртуального метода
Все методы, которые до сих пор рассматривались, имеют одну общую черту — все они статические . При обращении к статическому методу компилятор точно знает класс, которому данный метод принадлежит. Поэтому, например, обращение к статическому методу ParseLine в методе NextLine (принадлежащем классу TTextReader) компилируется в вызов TTextReader.ParseLine:
function TTextReader.NextLine: Boolean; var S: string; N: Integer; begin Result := not EndOfFile; if Result thenbegin Readln(FFile, S); N := ParseLine(S); // Компилируется в вызов TTextReader.ParseLine(S);if N <> ItemCount then SetLength(FItems, N); end; end;
В результате метод NextLine работает неправильно в наследниках класса TTextReader, так как внутри него вызов перекрытого метода ParseLine не происходит. Конечно, в классах TDelimitedReader и TFixedReader можно продублировать все методы и свойства, которые прямо или косвенно вызывают ParseLine, но при этом теряются преимущества наследования, и мы возвращаемся к тому, что необходимо описать два класса, в которых большая часть кода идентична. ООП предлагает изящное решение этой проблемы — метод ParseLine всего-навсего объявляется виртуальным :
type TTextReader = class . function ParseLine(const Line: string): Integer; virtual; //Виртуальный метод . end;
Объявление виртуального метода в базовом классе выполняется с помощью ключевого слова virtual , а его перекрытие в производных классах — с помощью ключевого слова override . Перекрытый метод должен иметь точно такой же формат (список параметров, а для функций еще и тип возвращаемого значения), что и перекрываемый:
type TDelimitedReader = class(TTextReader) . function ParseLine(const Line: string): Integer; override; . end; TFixedReader = class(TTextReader) . function ParseLine(const Line: string): Integer; override; . end;
Суть виртуальных методов в том, что они вызываются по фактическому типу экземпляра, а не по формальному типу, записанному в программе. Поэтому после сделанных изменений метод NextLine будет работать так, как ожидает программист:
function TTextReader.NextLine: Boolean; var S: string; N: Integer; begin Result := not EndOfFile; if Result thenbegin Readln(FFile, S); N := ParseLine(S); // Работает как .ParseLine(S)if N <> ItemCount then SetLength(FItems, N); end; end;
Работа виртуальных методов основана на механизме позднего связывания (late binding). В отличие от раннего связывания (early binding), характерного для статических методов, позднее связывание основано на вычислении адреса вызываемого метода при выполнении программы. Адрес метода вычисляется по хранящемуся в каждом объекте описателю класса.
Благодаря механизму наследования и виртуальных методов в среде Delphi реализуется такая концепция ООП как полиморфизм. Полиморфизм существенно облегчает труд программиста, поскольку обеспечивает повторное использование кода уже написанных и отлаженных методов.
3.8.2. Механизм вызова виртуальных методов
Работа виртуальных методов основана на косвенном вызове подпрограмм. При косвенном вызове команда вызова подпрограммы оперирует не адресом подпрограммы, а адресом места в памяти, где хранится адрес подпрограммы. Вы уже сталкивались с косвенным вызовом при использовании процедурных переменных. Процедурная переменная и была тем местом в памяти, где хранился адрес вызываемой подпрограммы. Для каждого виртуального метода тоже создается процедурная переменная, но ее наличие и использование скрыто от программиста.
Все процедурные переменные с адресами виртуальных методов пронумерованы и хранятся в таблице, называемой таблицей виртуальных методов (VMT — от англ. Virtual Method Table). Такая таблица создается одна для каждого класса объектов, и все объекты этого класса хранят на нее ссылку.
Структуру объекта в оперативной памяти поясняет рисунок 3.3:
Рисунок 3.3. Структура объекта TTextReader в оперативной памяти
Вызов виртуального метода осуществляется следующим образом:
- Через объектную переменную выполняется обращение к занятому объектом блоку памяти;
- Далее из этого блока извлекается адрес таблицы виртуальных методов (он записан в четырех первых байтах);
- На основании порядкового номера виртуального метода извлекается адрес соответствующей подпрограммы;
- Вызывается код, находящийся по этому адресу.
Покажем, как можно реализовать косвенный вызов виртуального метода ParseLine (он имеет нулевой номер в таблице виртуальных методов) обычными средствами процедурного программирования:
type TVMT = array[0..9999] of Pointer; TParseLineFunc = function (Self: TTextReader; const Line: string): Integer; var Reader: TTextReader; // объектная переменна ObjectDataPtr: Pointer; // указатель на занимаемый объектом блок памяти VMTPtr: ^TVMT; // указатель на таблицу виртуальных методов MethodPtr: Pointer; // указатель на методbegin . ObjectDataPtr := Pointer(Reader); // 1) обращение к данным объекта VMTPtr := Pointer(ObjectDataPtr^); // 2) извлечение адреса VMT MethodPtr := VMTPtr^[0]; // 3) извлечение адреса метода из VMT TParseLineFunc(MethodPtr)(Reader, S); // 4) вызов метода . end.
Поддержка механизма вызова виртуальных методов на уровне языка Delphi избавляет программиста от всей этой сложности.
3.8.3. Абстрактные виртуальные методы
При построении иерархии классов часто возникает ситуация, когда работа виртуального метода в базовом классе не известна и наполняется содержанием только в наследниках. Так случилось, например, с методом ParseLine, тело которого в классе TTextReader объявлено пустым. Конечно, тело метода всегда можно сделать пустым или почти пустым (так мы и поступили), но лучше воспользоваться директивой abstract :
type TTextReader = class . function ParseLine(const Line: string): Integer; virtual; abstract; . end;
Директива abstract записывается после слова virtual и исключает необходимость написания кода виртуального метода для данного класса. Такой метод называется абстрактным , т.е. подразумевает логическое действие, а не конкретный способ его реализации. Абстрактные виртуальные методы часто используются при создании классов-полуфабрикатов. Свою реализацию такие методы получают в законченных наследниках.
3.8.4. Динамические методы
Разновидностью виртуальных методов являются так называемые динамические методы . При их объявлении вместо ключевого слова virtual записывается ключевое слово dynamic , например:
type TTextReader = class . function ParseLine(const Line: string): Integer; dynamic; abstract; . end;
В наследниках динамические методы перекрываются так же, как и виртуальные — с помощью зарезервированного слова override .
По смыслу динамические и виртуальные методы идентичны. Различие состоит только в механизме их вызова. Методы, объявленные с директивой virtual , вызываются максимально быстро, но платой за это является большой размер системных таблиц, с помощью которых определяются их адреса. Размер этих таблиц начинает сказываться с увеличением числа классов в иерархии. Методы, объявленные с директивой dynamic вызываются несколько дольше, но при этом таблицы с адресами методов имеют более компактный вид, что способствует экономии памяти. Таким образом, программисту предоставляются два способа оптимизации объектов: по скорости работы ( virtual ) или по объему памяти ( dynamic ).
3.8.5. Методы обработки сообщений
Специализированной формой динамических методов являются методы обработки сообщений . Они объявляются с помощью ключевого слова message , за которым следует целочисленная константа — номер сообщения . Следующий пример взят из исходных текстов библиотеки VCL:
type TWidgetControl = class(TControl) . procedure CMKeyDown(var Msg: TCMKeyDown); message CM_KEYDOWN; . end;
Метод обработки сообщений имеет формат процедуры и содержит единственный var -параметр. При перекрытии такого метода название метода и имя параметра могут быть любыми, важно лишь, чтобы неизменным остался номер сообщения, используемый для вызова метода. Вызов метода выполняется не по имени, как обычно, а с помощью обращения к специальному методу Dispatch, который имеется в каждом классе (метод Dispatch определен в классе TObject).
Методы обработки сообщений применяются внутри библиотеки VCL для обработки команд пользовательского интерфейса и редко нужны при написании прикладных программ.
3.9. Классы в программных модулях
Классы очень удобно собирать в модули. При этом их описание помещается в секцию interface , а код методов — в секцию implementation . Создавая модули классов, нужно придерживаться следующих правил:
- все классы, предназначенные для использования за пределами модуля, следует определять в секции interface ;
- описание классов, предназначенных для употребления внутри модуля, следует располагать в секции implementation ;
- если модуль B использует модуль A, то в модуле B можно определять классы, порожденные от классов модуля A.
Соберем рассмотренные ранее классы TTextReader, TDelimitedReader и TFixedReader в отдельный модуль ReadersUnit:
unit ReadersUnit; interfacetype TTextReader = classprivate// Поля FFile: TextFile; FItems: arrayofstring; FActive: Boolean; // Методыprocedure PutItem(Index: Integer; const Item: string); // Методы чтения и записи свойствprocedure SetActive(const AActive: Boolean); function GetItemCount: Integer; function GetEndOfFile: Boolean; protected// Методы чтения и записи свойствfunction GetItem(Index: Integer): string; // Абстрактные методыfunction ParseLine(const Line: string): Integer; virtual; abstract; public// Конструкторы и деструкторыconstructor Create(const FileName: string); destructor Destroy; override; // Методыfunction NextLine: Boolean; // Свойстваproperty Active: Boolean read FActive write SetActive; property Items[Index: Integer]: stringread GetItem; default; property ItemCount: Integer read GetItemCount; property EndOfFile: Boolean read GetEndOfFile; end; TDelimitedReader = class(TTextReader) private// Поля FDelimiter: Char; protected// Методыfunction ParseLine(const Line: string): Integer; override; public// Конструкторы и деструкторыconstructor Create(const FileName: string; const ADelimiter: Char = ';'); // Свойстваproperty Delimiter: Char read FDelimiter; end; TFixedReader = class(TTextReader) private// Поля FItemWidths: arrayof Integer; protected// Методыfunction ParseLine(const Line: string): Integer; override; public// Конструкторы и деструкторыconstructor Create(const FileName: string; const AItemWidths: arrayof Integer); end; TMyReader = class(TDelimitedReader) property FirstName: stringindex 0 read GetItem; property LastName: stringindex 1 read GetItem; property Phone: stringindex 2 read GetItem; end; implementation constructor TTextReader.Create(const FileName: string); begininherited Create; AssignFile(FFile, FileName); FActive := False; end; destructor TTextReader.Destroy; begin Active := False; inherited; end; function TTextReader.GetEndOfFile: Boolean; begin Result := Eof(FFile); end; function TTextReader.GetItem(Index: Integer): string; begin Result := FItems[Index]; end; function TTextReader.GetItemCount: Integer; begin Result := Length(FItems); end; function TTextReader.NextLine: Boolean; var S: string; N: Integer; begin Result := not EndOfFile; if Result then// Если не достигнут конец файлаbegin Readln(FFile, S); // Чтение очередной строки из файла N := ParseLine(S); // Разбор считанной строкиif N <> ItemCount then SetLength(FItems, N); // Отсечение массива (если необходимо)end; end; procedure TTextReader.PutItem(Index: Integer; const Item: string); beginif Index > High(FItems) then// Если индекс выходит за границы массива, SetLength(FItems, Index + 1); // то увеличение размера массива FItems[Index] := Item; // Установка соответствующего элементаend; procedure TTextReader.SetActive(const AActive: Boolean); beginif Active <> AActive then// Если состояние изменяетсяbeginif AActive then Reset(FFile) // Открытие файлаelse CloseFile(FFile); // Закрытие файла FActive := AActive; // Сохранение состояния в полеend; end; constructor TDelimitedReader.Create(const FileName: string; const ADelimiter: Char = ';'); begininherited Create(FileName); FDelimiter := ADelimiter; end; function TDelimitedReader.ParseLine(const Line: string): Integer; var S: string; P: Integer; begin S := Line; Result := 0; repeat P := Pos(Delimiter, S); // Поиск разделителяif P = 0 then// Если разделитель не найден, то считается, что P := Length(S) + 1; // разделитель находится за последним символом PutItem(Result, Copy(S, 1, P - 1)); // Установка элемента Delete(S, 1, P); // Удаление элемента из строки Result := Result + 1; // Переход к следующему элементуuntil S = ''; // Пока в строке есть символыend; constructor TFixedReader.Create(const FileName: string; const AItemWidths: arrayof Integer); var I: Integer; begininherited Create(FileName); // Копирование AItemWidths в FItemWidths SetLength(FItemWidths, Length(AItemWidths)); for I := 0 to High(AItemWidths) do FItemWidths[I] := AItemWidths[I]; end; function TFixedReader.ParseLine(const Line: string): Integer; var I, P: Integer; begin P := 1; for I := 0 to High(FItemWidths) dobegin PutItem(I, Copy(Line, P, FItemWidths[I])); // Установка элемента P := P + FItemWidths[I]; // Переход к следующему элементуend; Result := Length(FItemWidths); // Количество элементов постоянноend; end.
Как можно заметить, в описании классов присутствуют новые ключевые слова private , protected и public . С их помощью регулируется видимость частей класса для других модулей и основной программы. Назначение каждого ключевого слова поясняется ниже.
3.10. Разграничение доступа к атрибутам объектов
Программист может разграничить доступ к атрибутам своих объектов для других программистов (и себя самого) с помощью специальных ключевых слов: 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++.
3.11. Указатели на методы объектов
В языке Delphi существуют процедурные типы данных для методов объектов. Внешне объявление процедурного типа для метода отличается от обычного словосочетанием of object , записанным после прототипа процедуры или функции:
type TReadLineEvent = procedure (Reader: TTextReader; const Line: string) ofobject;
Переменная такого типа называется указателем на метод (method pointer). Она занимает в памяти 8 байт и хранит одновременно ссылку на объект и адрес его метода.
type TTextReader = classprivate FOnReadLine: TReadLineEvent; . publicproperty OnReadLine: TReadLineEvent read FOnReadLine write FOnReadLine; end;
Методы объектов, объявленные по приведенному выше шаблону, становятся совместимы по типу со свойством OnReadLine.
type TForm1 = class(TForm) procedure HandleLine(Reader: TTextReader; const Line: string); end; var Form1: TForm1; Reader: TTextReader;
Если установить значение свойства OnReadLine:
Reader.OnReadLine := Form1.HandleLine;
и переписать метод NextLine,
function TTextReader.NextLine: Boolean; var S: string; N: Integer; begin Result := not EndOfFile; if Result then// Если строки для считывания еще есть, тоbegin Readln(FFile, S); // Считывание очередной строки N := ParseLine(S); // Выделение элементов строки (разбор строки)if N <> ItemCount then SetLength(FItems, N); if Assigned(FOnReadLine) then FOnReadLine(Self, S); // уведомление о чтении очередной строкиend; end;
то объект Form1 через метод HandleLine получит уведомление об очередной считанной строке. Обратите внимание, что вызов метода через указатель происходит лишь в том случае, если указатель не равен nil . Эта проверка выполняется с помощью стандартной функции Assigned, которая возвращает True, если ее аргумент является связанным указателем.
Описанный выше механизм называется делегированием , поскольку он позволяет передать часть работы другому объекту, например, сосредоточить в одном объекте обработку событий, возникающих в других объектах. Это избавляет программиста от необходимости порождать многочисленные классы-наследники и перекрывать в них виртуальные методы. Делегирование широко применяется в среде Delphi. Например, все компоненты делегируют обработку своих событий той форме, в которую они помещены.
3.12. Метаклассы
3.12.1. Ссылки на классы
Язык Delphi позволяет рассматривать классы объектов как своего рода объекты, которыми можно манипулировать в программе. Такая возможность рождает новое понятие — класс класса ; его принято обозначать термином метакласс .
Для поддержки метаклассов введен специальный тип данных — ссылка на класс (class reference). Он описывается с помощью словосочетания class of , например:
type TTextReaderClass = classof TTextReader;
Переменная типа TTextReaderClass объявляется в программе обычным образом:
var ClassRef: TTextReaderClass;
Значениями переменной ClassRef могут быть класс TTextReader и все порожденные от него классы. Допустимы следующие операторы:
ClassRef := TTextReader; ClassRef := TDelimitedReader; ClassRef := TFixedReader;
По аналогии с тем, как для всех классов существует общий предок TObject, у ссылок на классы существует базовый тип TClass, определенный, как:
type TClass = classof TObject;
Переменная типа TClass может ссылаться на любой класс.
Практическая ценность ссылок на классы состоит в возможности создавать программные модули, работающие с любыми классами объектов, даже теми, которые еще не разработаны.
Физический смысл и взаимосвязь таких понятий, как переменная-объект, экземпляр объекта в памяти, переменная-класс и экземпляр класса в памяти поясняет рисунок 3.4.
Рисунок 3.4. Переменная-объект, экземпляр объекта в памяти, переменная-класс и экземпляр класса в памяти
3.12.2. Методы классов
Метаклассы привели к возникновению нового типа методов — методов класса. Метод класса оперирует не экземпляром объекта, а непосредственно классом. Он объявляется как обычный метод, но перед словом procedure или function записывается зарезервированное слово class , например:
type TTextReader = class . classfunction GetClassName: string; end;
Передаваемый в метод класса неявный параметр Self содержит не ссылку на объект, а ссылку на класс, поэтому в теле метода нельзя обращаться к полям, методам и свойствам объекта. Зато можно вызывать другие методы класса, например:
class function TTextReader.GetClassName: string; begin Result := ClassName; end;
Метод ClassName объявлен в классе TObject и возвращает имя класса, к которому применяется. Очевидно, что надуманный метод GetClassName просто дублирует эту функциональность для класса TTextReader и всех его наследников.
Методы класса применимы и к классам, и к объектам. В обоих случаях в параметре Self передается ссылка на класс объекта. Пример:
var Reader: TTextReader; S: string; begin// Вызов метода с помощью ссылки на класс S := TTextReader.GetClassName; // S получит значение 'TTextReader'// Создание объекта класса TDelimitedReader Reader := TDelimitedReader.Create('MyData.del'); // Вызов метода с помощью ссылки на объект S := Reader.GetClassName; // S получит значение 'TDelimitedReader'end.
Методы классов могут быть виртуальными. Например, в классе TObject определен виртуальный метод класса NewInstance. Он служит для распределения памяти под объект и автоматически вызывается конструктором. Его можно перекрыть в своем классе, чтобы обеспечить нестандартный способ выделения памяти для экземпляров. Метод NewInstance должен перекрываться вместе с другим методом FreeInstance, который автоматически вызывается из деструктора и служит для освобождения памяти. Добавим, что размер памяти, требуемый для экземпляра, можно узнать вызовом предопределенного метода класса InstanceSize.
3.12.3. Виртуальные конструкторы
Особая прелесть ссылок на классы проявляется в сочетании с виртуальными конструкторами. Виртуальный конструктор объявляется с ключевым словом virtual . Вызов виртуального конструктора происходит по фактическому значению ссылки на класс, а не по ее формальному типу. Это позволяет создавать объекты, классы которых неизвестны на этапе компиляции. Механизм виртуальных конструкторов применяется в среде Delphi при восстановлении компонентов формы из файла. Восстановление компонента происходит следующим образом. Из файла считывается имя класса. По этому имени отыскивается ссылка на класс (метакласс). У метакласса вызывается виртуальный конструктор, который создает объект нужного класса.
var P: TComponent; T: TComponentClass; // TComponentClass = class of TComponent; . T := FindClass(ReadStr); P := T.Create(nil); .
На этом закончим изучение теории объектно-ориентированного программирования и в качестве практики рассмотрим несколько широко используемых инструментальных классов среды Delphi. Разберитесь с их назначением и работой. Это поможет глубже понять ООП и пригодится на будущее.
3.13. Классы общего назначения
Как показывает практика, в большинстве задач приходится использовать однотипные структуры данных: списки, массивы, множества и т.д. От задачи к задаче изменяются только их элементы, а методы работы сохраняются. Например, для любого списка нужны процедуры вставки и удаления элементов. В связи с этим возникает естественное желание решить задачу «в общем виде», т.е. создать универсальные средства для управления основными структурами данных. Эта идея не нова. Она давно пришла в голову разработчикам инструментальных пакетов, которые быстро наплодили множество вспомогательных библиотек. Эти библиотеки содержали классы объектов для работы со списками, коллекциями (динамические массивы с переменным количеством элементов), словарями (коллекции, индексированные строками) и другими «абстрактными» структурами. Для среды Delphi тоже разработаны аналогичные классы объектов. Их большая часть сосредоточена в модуле Classes. Наиболее нужными для вас являются списки строк (TStrings, TStringList) и потоки (TSream, THandleSream, TFileStream, TMemoryStream и TBlobStream). Рассмотрим кратко их назначение и применение.
3.13.1. Классы для представления списка строк
Для работы со списками строк служат классы 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 генерируется перед тем, как будут сделаны изменения.
Ниже приводится фрагмент программы, демонстрирующий создание списка строк и манипулирование его элементами:
var Items: TStrings; I: Integer; begin// Создание списка Items := TStringList.Create; Items.Add('Туризм'); Items.Add('Наука'); Items.Insert(1, 'Бизнес'); . // Работа со спискомfor I := 0 to Items.Count - 1 do Items[I] := UpperCase(Items[I]); . // Удаление списка Items.Free; end;
3.13.2. Классы для представления потока данных
В среде Delphi существует иерархия классов для хранения и последовательного ввода-вывода данных. Классы этой иерархии называются потоками . Потоки лучше всего представлять как файлы. Классы потоков обеспечивают различное физическое представление данных: файл на диске, раздел оперативной памяти, поле в таблице базы данных (таблица 3.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 байт вперед. Если по какой-либо причине невозможно записать все байты буфера, то генерируется ошибка.
Ниже приводится фрагмент программы, демонстрирующий создание файлового потока и запись в него строки:
var Stream: TStream; S: AnsiString; StrLen: Integer; begin// Создание файлового потока Stream := TFileStream.Create('Sample.Dat', fmCreate); . // Запись в поток некоторой строки StrLen := Length(S) * SizeOf(Char); Stream.Write(StrLen, SizeOf(Integer)); // запись длины строки Stream.Write(S, StrLen); // запись символов строки . // Закрытие потока Stream.Free; end;
3.14. Итоги
Теперь для вас нет секретов в мире ООП. Вы на достаточно серьезном уровне познакомились с объектами и их свойствами; узнали, как объекты создаются, используются и уничтожаются. Если не все удалось запомнить сразу — не беда. Возвращайтесь к материалам главы по мере решения стоящих перед вами задач, и работа с объектами станет простой, естественной и даже приятной. Когда вы достигните понимания того, как работает один объект, то автоматически поймете, как работают все остальные. Теперь мы рассмотрим то, с чем вы встретитесь очень скоро — ошибки программирования.
Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.