Программирование на языке Delphi. Глава 3. Объектно-ориентированное программирование (ООП). Часть 2

Источник: RSDNru
А.Н. Вальвачев, К.А. Сурков, Д.А. Сурков, Ю.М. Четырько

предыдущая статья серии

Оглавление

Наследование

Понятие наследования

Классы инкапсулируют (т.е. включают в себя) поля, методы и свойства; это их первая черта. Следующая не менее важная черта классов - способность наследовать поля, методы и свойства других классов. Чтобы пояснить сущность наследования обратимся к примеру с читателем текстовых файлов в формате "delimited text".

Класс TDelimitedReader описывает объекты для чтения из текстового файла элементов, разделенных некоторым символом. Он не пригоден для чтения элементов, хранящихся в другом формате, например в формате с фиксированным количеством символов для каждого элемента. Для этого необходим другой класс:

type
 TFixedReader = class
 private
 // Поля
 FFile: TextFile;
 FItems: array of string;
 FActive: Boolean;
 FItemWidths: array of 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: array of Integer);
 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;

{ TFixedReader }

constructor TFixedReader.Create(const FileName: string;
 const AItemWidths: array of 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) do
 begin
 PutItem(I, Copy(Line, P, FItemWidths[I])); // Установка элемента
 P := P + FItemWidths[I]; // Переход к следующему элементу
 end;
 Result := Length(FItemWidths); // Количество элементов постоянно
end;

procedure TFixedReader.PutItem(Index: Integer; const Item: string);
begin
 if Index > High(FItems) then // Если индекс выходит за границы массива,
 SetLength(FItems, Index + 1); // то увеличение размера массива
 FItems[Index] := Item; // Установка соответствующего элемента
end;

procedure TFixedReader.SetActive(const AActive: Boolean);
begin
 if Active <> AActive then // Если состояние изменяется
 begin
 if AActive then
 Reset(FFile) // Открытие файла
 else
 CloseFile(FFile); // Закрытие файла
 FActive := AActive; // Сохранение состояния в поле
 end;
end;

Поля, свойства и методы класса TFixedReader практически полностью аналогичны тем, что определены в классе TDelimitedReader. Отличие состоит в отсутствии свойства Delimiter, наличии поля FItemWidths (для хранения размеров элементов), другой реализации метода ParseLine и немного отличающемся конструкторе. Если в будущем появится класс для чтения элементов из файла еще одного формата (например, зашифрованного текста), то придется снова определять общие для всех классов поля, методы и свойства. Чтобы избавиться от дублирования общих атрибутов (полей, свойств и методов) при определении новых классов, воспользуемся механизмом наследования. Прежде всего, выделим в отдельный класс TTextReader общие атрибуты всех классов, предназначенных для чтения элементов из текстовых файлов. Реализация методов TTextReader, кроме метода ParseLine, полностью идентична реализации TDelimitedReader, приведенной в предыдущем разделе.

type
 TTextReader = class
 private
 // Поля
 FFile: TextFile;
 FItems: array of string;
 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: array of Integer;
 function ParseLine(const Line: string): Integer; override;
 constructor Create(const FileName: string;
 const AItemWidths: array of Integer);
 end;
...

Классы TDelimitedReader и TFixedReader определены как наследники TTextReader (об этом говорит имя в скобках после слова class). Они автоматически включают в себя все описания, сделанные в классе TTextReader и добавляют к ним некоторые новые. В результате формируется дерево классов, показанное на рисунке 1 (оно всегда рисуется перевернутым).

Рисунок 1. Дерево классов

Рисунок 1. Дерево классов

Класс, который наследует атрибуты другого класса, называется порожденным классом или потомком. Соответственно класс, от которого происходит наследование, выступает в роли базового, или предка. В нашем примере класс TDelimitedReader является прямым потомком класса TTextReader. Если от TDelimitedReader породить новый класс, то он тоже будет потомком класса TTextReader, но уже не прямым.

Очень важно, что в отношениях наследования любой класс может иметь только одного непосредственного предка и сколь угодно много потомков. Поэтому все связанные отношением наследования классы образуют иерархию. Примером иерархии классов является библиотека VCL; с ее помощью в среде Delphi обеспечивается разработка GUI-приложений.

Прародитель всех классов

В языке Delphi существует предопределенный класс TObject, который служит неявным предком тех классов, для которых предок не указан. Это означает, что объявление

type
 TTextReader = class
 ...
 end;

эквивалентно следующему:

type
 TTextReader = class(TObject)
 ...
 end;

Класс TObject выступает корнем любой иерархии классов. Он содержит ряд методов, которые по наследству передаются всем остальным классам. Среди них конструктор Create, деструктор Destroy, метод Free и некоторые другие методы.

Таким образом, полное дерево классов для чтения элементов из текстового файла в различных форматах выглядит так, как показано на рисунке 2.

Рисунок 2. Полное дерево классов

Рисунок 2. Полное дерево классов

Поскольку класс TObject является предком для всех других классов (в том числе и для ваших собственных), то не лишним будет кратко ознакомиться с его методами:

type
 TObject = class
 constructor Create;
 procedure Free;
 class function InitInstance(Instance: Pointer): TObject;
 procedure CleanupInstance;
 function ClassType: TClass;
 class function ClassName: ShortString;
 class function ClassNameIs(const Name: string): Boolean;
 class function ClassParent: TClass;
 class function ClassInfo: Pointer;
 class function InstanceSize: Longint;
 class function InheritsFrom(AClass: TClass): Boolean;
 class function MethodAddress(const Name: ShortString): Pointer;
 class function MethodName(Address: Pointer): ShortString;
 function FieldAddress(const Name: ShortString): Pointer;
 function GetInterface(const IID: TGUID; out Obj): Boolean;
 class function GetInterfaceEntry(const IID: TGUID): PInterfaceEntry;
 class function 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;
 class function 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 - стандартный деструктор.

Перекрытие атрибутов в наследниках

В механизме наследования можно условно выделить три основных момента:

  • наследование полей;
  • наследование свойств;
  • наследование методов.

Любой порожденный класс наследует от родительского все поля данных, поэтому классы 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) do
 begin
 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 = ';');
begin
 inherited Create(FileName);
 FDelimiter := ADelimiter;
end;

constructor TFixedReader.Create(const FileName: string;
 const AItemWidths: array of Integer);
var
 I: Integer;
begin
 inherited 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;

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

Совместимость объектов различных классов

Для классов, связанных отношением наследования, вводится новое правило совместимости типов. Вместо объекта базового класса можно подставить объект любого производного класса. Обратное неверно. Например, переменной типа TTextReader можно присвоить значение переменной типа TDelimitedReader:

var
 Reader: TTextReader;
...
 Reader := TDelimitedReader.Create('MyData.del', ';');

Объектная переменная Reader формально имеет тип TTextReader, а фактически связана с экземпляром класса TDelimitedReader.

Правило совместимости классов чаще всего применяется при передаче объектов в параметрах процедур и функций. Например, если процедура работает с объектом класса TTextReader, то вместо него можно передать объект класса TDelimitedReader или TFixedReader.

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

Контроль и преобразование типов

Поскольку реальный экземпляр объекта может оказаться наследником класса, указанного при описании объектной переменной или параметра, бывает необходимо проверить, к какому классу принадлежит объект на самом деле. Чтобы программист мог выполнять такого рода проверки, каждый объект хранит информацию о своем классе. В языке 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. Забегая вперед, скажем, что ошибку приведения типа можно обработать и таким образом избежать досрочного завершения программы.

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

Понятие виртуального метода

Все методы, которые до сих пор рассматривались, имеют одну общую черту - все они статические. При обращении к статическому методу компилятор точно знает класс, которому данный метод принадлежит. Поэтому, например, обращение к статическому методу ParseLine в методе NextLine (принадлежащем классу TTextReader) компилируется в вызов TTextReader.ParseLine:

function TTextReader.NextLine: Boolean;
var
 S: string;
 N: Integer;
begin
 Result := not EndOfFile;
 if Result then 
 begin
 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 then 
 begin
 Readln(FFile, S); 
 N := ParseLine(S); // Работает как <фактический класс>.ParseLine(S)
 if N <> ItemCount then
 SetLength(FItems, N); 
 end;
end;

Работа виртуальных методов основана на механизме позднего связывания (late binding). В отличие от раннего связывания (early binding), характерного для статических методов, позднее связывание основано на вычислении адреса вызываемого метода при выполнении программы. Адрес метода вычисляется по хранящемуся в каждом объекте описателю класса.

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

Механизм вызова виртуальных методов

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

Все процедурные переменные с адресами виртуальных методов пронумерованы и хранятся в таблице, называемой таблицей виртуальных методов (VMT - от англ. Virtual Method Table). Такая таблица создается одна для каждого класса объектов, и все объекты этого класса хранят на нее ссылку.

Структуру объекта в оперативной памяти поясняет рисунок 3:

Рисунок 3. Структура объекта TTextReader в оперативной памяти

Рисунок 3. Структура объекта TTextReader в оперативной памяти

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

  1. Через объектную переменную выполняется обращение к занятому объектом блоку памяти;
  2. Далее из этого блока извлекается адрес таблицы виртуальных методов (он записан в четырех первых байтах);
  3. На основании порядкового номера виртуального метода извлекается адрес соответствующей подпрограммы;
  4. Вызывается код, находящийся по этому адресу.

Покажем, как можно реализовать косвенный вызов виртуального метода 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 избавляет программиста от всей этой сложности.

Абстрактные виртуальные методы

При построении иерархии классов часто возникает ситуация, когда работа виртуального метода в базовом классе не известна и наполняется содержанием только в наследниках. Так случилось, например, с методом ParseLine, тело которого в классе TTextReader объявлено пустым. Конечно, тело метода всегда можно сделать пустым или почти пустым (так мы и поступили), но лучше воспользоваться директивой abstract:

type
 TTextReader = class
 ...
 function ParseLine(const Line: string): Integer; virtual; abstract; 
 ...
 end;

Директива abstract записывается после слова virtual и исключает необходимость написания кода виртуального метода для данного класса. Такой метод называется абстрактным, т.е. подразумевает логическое действие, а не конкретный способ его реализации. Абстрактные виртуальные методы часто используются при создании классов-полуфабрикатов. Свою реализацию такие методы получают в законченных наследниках.

Динамические методы

Разновидностью виртуальных методов являются так называемые динамические методы. При их объявлении вместо ключевого слова virtual записывается ключевое слово dynamic, например:

type
 TTextReader = class
 ...
 function ParseLine(const Line: string): Integer; dynamic; abstract; 
 ...
 end;

В наследниках динамические методы перекрываются так же, как и виртуальные - с помощью зарезервированного слова override.

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

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

Специализированной формой динамических методов являются методы обработки сообщений. Они объявляются с помощью ключевого слова message, за которым следует целочисленная константа - номер сообщения. Следующий пример взят из исходных текстов библиотеки VCL:

type
 TWidgetControl = class(TControl)
 ...
 procedure CMKeyDown(var Msg: TCMKeyDown); message CM_KEYDOWN;
 ...
 end;

Метод обработки сообщений имеет формат процедуры и содержит единственный var-параметр. При перекрытии такого метода название метода и имя параметра могут быть любыми, важно лишь, чтобы неизменным остался номер сообщения, используемый для вызова метода. Вызов метода выполняется не по имени, как обычно, а с помощью обращения к специальному методу Dispatch, который имеется в каждом классе (метод Dispatch определен в классе TObject).

Методы обработки сообщений применяются внутри библиотеки VCL для обработки команд пользовательского интерфейса и редко нужны при написании прикладных программ.

следующая статья серии

Ссылки по теме


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=1482