Вы находитесь на страницах старой версии сайта.
Переходите на новую версию Interface.Ru

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

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

© А.Н. Вальвачев, К.А. Сурков, Д.А. Сурков, Ю.М. Четырько
Статья была опубликована на сайте rsdn.ru

Классы в программных модулях

Классы очень удобно собирать в модули. При этом их описание помещается в секцию interface, а код методов — в секцию implementation. Создавая модули классов, нужно придерживаться следующих правил:

Соберем рассмотренные ранее классы TTextReader, TDelimitedReader и TFixedReader в отдельный модуль ReadersUnit:

unit ReadersUnit;

interface

type
  TTextReader = class
  private
    // Поля
    FFile: TextFile;
    FItems: array of string;
    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]: string read 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: array of Integer;
  protected
    // Методы
    function ParseLine(const Line: string): Integer; override;
  public
    // Конструкторы и деструкторы
    constructor Create(const FileName: string;
      const AItemWidths: array of Integer);
  end;

  TMyReader = class(TDelimitedReader)
    property FirstName: string index 0 read GetItem;
    property LastName: string index 1 read GetItem;
    property Phone: string index 2 read GetItem;
  end;

implementation

{ TTextReader }

constructor TTextReader.Create(const FileName: string);
begin
  inherited 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);
begin
  if Index > High(FItems) then    // Если индекс выходит за границы массива,
    SetLength(FItems, Index + 1); // то увеличение размера массива
  FItems[Index] := Item;          // Установка соответствующего элемента
end;

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

{ TDelimitedReader }

constructor TDelimitedReader.Create(const FileName: string;
  const ADelimiter: Char = ';');
begin
  inherited 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;

{ TFixedReader }

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;

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;

end.

Как можно заметить, в описании классов присутствуют новые ключевые слова private, protected и public. С их помощью регулируется видимость частей класса для других модулей и основной программы. Назначение каждого ключевого слова поясняется ниже.

Разграничение доступа к атрибутам объектов

Программист может разграничить доступ к атрибутам своих объектов для других программистов (и себя самого) с помощью специальных ключевых слов: private, protected, public, published (последнее не используется в модуле ReadersUnit).

Перечисленные секции могут чередоваться в объявлении класса в произвольном порядке, однако в пределах секции сначала следует описание полей, а потом методов и свойств. Если в определении класса нет ключевых слов private, protected, public и published, то для обычных классов всем полям, методам и свойствам приписывается атрибут видимости public, а для тех классов, которые порождены от классов библиотеки VCL, — атрибут видимости published.

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

Указатели на методы объектов

В языке Delphi существуют процедурные типы данных для методов объектов. Внешне объявление процедурного типа для метода отличается от обычного словосочетанием of object, записанным после прототипа процедуры или функции:

type
  TReadLineEvent = procedure (Reader: TTextReader; const Line: string) of object;

Переменная такого типа называется указателем на метод (method pointer). Она занимает в памяти 8 байт и хранит одновременно ссылку на объект и адрес его метода.

type
  TTextReader = class
  private
    FOnReadLine: TReadLineEvent;
    ...
  public
    property 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. Например, все компоненты делегируют обработку своих событий той форме, в которую они помещены.

Метаклассы

Ссылки на классы

Язык Delphi позволяет рассматривать классы объектов как своего рода объекты, которыми можно манипулировать в программе. Такая возможность рождает новое понятие — класс класса; его принято обозначать термином метакласс.

Для поддержки метаклассов введен специальный тип данных — ссылка на класс (class reference). Он описывается с помощью словосочетания class of, например:

type
  TTextReaderClass = class of TTextReader;

Переменная типа TTextReaderClass объявляется в программе обычным образом:

var
  ClassRef: TTextReaderClass;

Значениями переменной ClassRef могут быть класс TTextReader и все порожденные от него классы. Допустимы следующие операторы:

ClassRef := TTextReader;
ClassRef := TDelimitedReader;
ClassRef := TFixedReader;

По аналогии с тем, как для всех классов существует общий предок TObject, у ссылок на классы существует базовый тип TClass, определенный, как:

type
  TClass = class of TObject;

Переменная типа TClass может ссылаться на любой класс.

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

Физический смысл и взаимосвязь таких понятий, как переменная-объект, экземпляр объекта в памяти, переменная-класс и экземпляр класса в памяти поясняет рисунок 4.

Рисунок 4. Переменная-объект, экземпляр объекта в памяти, переменная-класс и экземпляр класса в памяти

Рисунок 4. Переменная-объект, экземпляр объекта в памяти, переменная-класс и экземпляр класса в памяти

Методы классов

Метаклассы привели к возникновению нового типа методов — методов класса. Метод класса оперирует не экземпляром объекта, а непосредственно классом. Он объявляется как обычный метод, но перед словом procedure или function записывается зарезервированное слово class, например:

type
  TTextReader = class
    ...
    class function 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.

Виртуальные конструкторы

Особая прелесть ссылок на классы проявляется в сочетании с виртуальными конструкторами. Виртуальный конструктор объявляется с ключевым словом virtual. Вызов виртуального конструктора происходит по фактическому значению ссылки на класс, а не по ее формальному типу. Это позволяет создавать объекты, классы которых неизвестны на этапе компиляции. Механизм виртуальных конструкторов применяется в среде Delphi при восстановлении компонентов формы из файла. Восстановление компонента происходит следующим образом. Из файла считывается имя класса. По этому имени отыскивается ссылка на класс (метакласс). У метакласса вызывается виртуальный конструктор, который создает объект нужного класса.

var
  P: TComponent;
  T: TComponentClass;  // TComponentClass = class of TComponent;
...
  T := FindClass(ReadStr);
  P := T.Create(nil);
...

На этом закончим изучение теории объектно-ориентированного программирования и в качестве практики рассмотрим несколько широко используемых инструментальных классов среды Delphi. Разберитесь с их назначением и работой. Это поможет глубже понять ООП и пригодится на будущее.

Классы общего назначения

Как показывает практика, в большинстве задач приходится использовать однотипные структуры данных: списки, массивы, множества и т.д. От задачи к задаче изменяются только их элементы, а методы работы сохраняются. Например, для любого списка нужны процедуры вставки и удаления элементов. В связи с этим возникает естественное желание решить задачу "в общем виде", т.е. создать универсальные средства для управления основными структурами данных. Эта идея не нова. Она давно пришла в голову разработчикам инструментальных пакетов, которые быстро наплодили множество вспомогательных библиотек. Эти библиотеки содержали классы объектов для работы со списками, коллекциями (динамические массивы с переменным количеством элементов), словарями (коллекции, индексированные строками) и другими "абстрактными" структурами. Для среды Delphi тоже разработаны аналогичные классы объектов. Их большая часть сосредоточена в модуле Classes. Наиболее нужными для вас являются списки строк (TStrings, TStringList) и потоки (TSream, THandleSream, TFileStream, TMemoryStream и TBlobStream). Рассмотрим кратко их назначение и применение.

Классы для представления списка строк

Для работы со списками строк служат классы TStrings и TStringList. Они используются в библиотеке VCL повсеместно и имеют гораздо большую универсальность, чем та, что можно почерпнуть из их названия. Классы TStrings и TStringList служат для представления не просто списка строк, а списка элементов, каждый из которых представляет собой пару строка-объект. Если со строками не ассоциированы объекты, получается обычный список строк.

Класс TStrings используется визуальными компонентами и является абстрактным. Он не имеет собственных средств хранения строк и определяет лишь интерфейс для работы с элементами. Класс TStringList является наследником TStrings и служит для организации списков строк, которые используются отдельно от управляющих элементов. Объекты TStringList хранят строки и объекты в динамической памяти.

Свойства класса TStrings описаны ниже.

Наследники класса TStrings иногда используются для хранения строк вида Имя=Значение, в частности, строк INI-файлов (см. гл. 6). Для удобной работы с такими строками в классе TStrings дополнительно имеются следующие свойства.

Управление элементами списка осуществляется с помощью следующих методов:

Класс TStringList добавляет к TStrings несколько дополнительных свойств и методов, а также два свойства-события для уведомления об изменениях в списке. Они описаны ниже.

Свойства:

Методы:

События:

Ниже приводится фрагмент программы, демонстрирующий создание списка строк и манипулирование его элементами:

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;

Классы для представления потока данных

В среде Delphi существует иерархия классов для хранения и последовательного ввода-вывода данных. Классы этой иерархии называются потоками. Потоки лучше всего представлять как файлы. Классы потоков обеспечивают различное физическое представление данных: файл на диске, раздел оперативной памяти, поле в таблице базы данных (таблица 1).

Таблица 1. Классы потоков

Класс

Описание

TStreamАбстрактный поток, от которого наследуются все остальные. Свойства и методы класса TStream образуют базовый интерфейс потоковых объектов.
THandleStreamПоток, который хранит свои данные в файле. Для чтения-записи файла используется дескриптор (handle), поэтому поток называется дескрипторным. Дескриптор — это номер открытого файла в операционной системе. Его возвращают низкоуровневые функции создания и открытия файла.
TFileStreamПоток, который хранит свои данные в файле. Отличается от ThandleStream тем, что сам открывает (создает) файл по имени, переданному в конструктор.
TMemoryStreamПоток, который хранит свои данные в оперативной памяти. Моделирует работу с файлом. Используется для хранения промежуточных результатов, когда файловый поток не подходит из-за низкой скорости передачи данных.
TResourceStreamПоток, обеспечивающий доступ к ресурсам в Windows-приложении.
TBlobStreamОбеспечивает последовательный доступ к большим полям таблиц в базах данных.

Потоки широко применяются в библиотеке VCL и наверняка вам понадобятся. Поэтому ниже кратко перечислены их основные общие свойства и методы.

Общие свойства:

Общие методы:

Ниже приводится фрагмент программы, демонстрирующий создание файлового потока и запись в него строки:

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;

Итоги

Теперь для вас нет секретов в мире ООП. Вы на достаточно серьезном уровне познакомились с объектами и их свойствами; узнали, как объекты создаются, используются и уничтожаются. Если не все удалось запомнить сразу — не беда. Возвращайтесь к материалам главы по мере решения стоящих перед вами задач, и работа с объектами станет простой, естественной и даже приятной. Когда вы достигните понимания того, как работает один объект, то автоматически поймете, как работают все остальные. Теперь мы рассмотрим то, с чем вы встретитесь очень скоро — ошибки программирования.

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

Дополнительная информация

За дополнительной информацией обращайтесь в компанию Interface Ltd.

Обсудить на форуме Borland

Рекомендовать страницу

INTERFACE Ltd.
Телефон/Факс: +7 (495) 925-0049
Отправить E-Mail
http://www.interface.ru
Rambler's Top100
Ваши замечания и предложения отправляйте редактору
По техническим вопросам обращайтесь к вебмастеру
Дата публикации: 16.03.06