|
|
|||||||||||||||||||||||||||||
|
Программирование на языке Delphi. Глава 3. Объектно-ориентированное программирование (ООП). Часть 3Источник: RSDNru А.Н. Вальвачев, К.А. Сурков, Д.А. Сурков, Ю.М. Четырько
Оглавление
Классы в программных модуляхКлассы очень удобно собирать в модули. При этом их описание помещается в секцию 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. Переменная-объект, экземпляр объекта в памяти, переменная-класс и экземпляр класса в памяти Методы классовМетаклассы привели к возникновению нового типа методов - методов класса. Метод класса оперирует не экземпляром объекта, а непосредственно классом. Он объявляется как обычный метод, но перед словом 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. Классы потоков
Потоки широко применяются в библиотеке 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; ИтогиТеперь для вас нет секретов в мире ООП. Вы на достаточно серьезном уровне познакомились с объектами и их свойствами; узнали, как объекты создаются, используются и уничтожаются. Если не все удалось запомнить сразу - не беда. Возвращайтесь к материалам главы по мере решения стоящих перед вами задач, и работа с объектами станет простой, естественной и даже приятной. Когда вы достигните понимания того, как работает один объект, то автоматически поймете, как работают все остальные. Теперь мы рассмотрим то, с чем вы встретитесь очень скоро - ошибки программирования. Ссылки по теме
|
|