|
Навигация
|
Главная » Delphi Фундаментально про объектно-ориентированное программированиеИсточник: DelphiSources Федоренко Сергей Введение Язык программирования Object Pascal и его достойный преемник, среда программирования Delphi, построены на основе получившей широкое развитие на стыке 70 - 80-х годов 20 века теории объектно-ориентированного программирования. В то время идея описания программ в базисе логических сущностей и взаимодействия между ними не была такой уж бесспорной, а у некоторых оппонентов даже вызывала определённое недоумение. Преимущества ООП по сравнению с традиционными способами программирования:
type TPeople = class Name: string; Family: string; procedure GetName; procedure GetFamily; end;Класс содержит поля (Name, Family) и методы (GetName, GetFamily). Заголовки методов, всегда следующие за списком полей, играют роль упреждающих описаний. Программный код методов пишется отдельно от определения класса и будет приведён позже. Чтобы от описания класса перейти к объекту, следует выполнить следующее объявление в секции var: var People: TPeople; При работе с обычными типами данных этого объявления было бы достаточно для получения экземпляра типа. Однако объекты среды Delphi являются динамическими данными, то есть распределяются в динамической памяти. Поэтому переменная People - это просто ссылка на экземпляр (объект в памяти), которого физически ещё не существует. Чтобы сконструировать объект (выделить память для экземпляра) класса TPeople и связать с ним переменную People, нужно в тексте программы поместить следующий оператор: People := TPeople.Create; //Выделение памяти под объект Create - это так называемый конструктор объекта; он всегда присутствует в классе и служит для создания и инициализации экземпляров. При создании объекта в памяти выделяется место только для его полей. Методы, как и обычные процедуры и функции, помещаются в область кода программы; они умеют работать с любыми экземплярами своего класса и не дублируются в памяти. После создания объект можно использовать в программе: получать и устанавливать значения его полей, вызывать его методы. Доступ к полям и методам объекта происходит с помощью уточнённых имён, например: People.GetName; People.GetFamily; Кроме того, как и при работе с записями, допустимо использование оператора with, например: with People do GetFamily; GetName; Если объект становится ненужным, он должен быть удалён вызова специального метода Destroy, например: People.Destroy; //Освобождение памяти, занимаемой объектом Destroy - это так называемый деструктор объекта; он присутствует в классе наряду с конструктором и служит для удаления объекта из динамической памяти. После вызова деструктора переменная People становится несвязанной и не должна использоваться для доступа к полям и методам уже несуществующего объекта. Чтобы отличать в программе связанные объектные переменные от несвязанных, последнее следует инициализировать значением nil. Пример: People := nil; if People <> nil then People.Destroy; Вызов деструктора для несуществующих объектов недопустим и при выполнении программы приведёт к ошибке. Чтобы избавить программистов от лишних ошибок, в объекты ввели предоставленный метод Free, который следует вызвать вместо деструктора. Метод Free сам вызывает деструктор Destroy, но только в том случае, если значение объектной переменной не равно nil. Поэтому последнюю строчку в приведённом выше примере можно переписать следующим образом: People.Free; После уничтожения объекта переменная People сохраняет своё значение, продолжая ссылаться на место в памяти, где объекта уже нет. Если эту переменную предполагается ещё использовать, то желательно присвоить ей значение nil, чтобы программа могла проверить, существует объект или нет. Таким образом, наиболее правильная последовательность действий при уничтожении объекта должна быть следующая: People. Free; People := nil; С помощью стандартной процедуры FreeAndNil это можно сделать проще и элегантнее: FreeAndNil(People); //Эквивалентно: People. Free; People := nil; Значение одной объектной переменной можно присвоить другой. При этом объект не копируется в памяти, а вторая переменная просто связывается с тем же объектом, что и первая: var People1, People2: TPeople; begin People1 := TPeople.Create; People1 := People2; People. Free; end; Объекты могут выступать в программе не только в качестве переменных, но также элементов массивов, полей записей, параметров процедур и функций. Кроме того, они могут служить полями других объектов. Во всех этих случаях программист фактически оперирует указателями на экземпляры объектов в динамической памяти. Следовательно, объекты изначально приспособлены для создания сложных динамических структур данных, таких как списки и деревья. Указатели на объекты для этого не нужны. В некоторых случаях требуется, чтобы объекты разных классов содержали ссылки друг на друга. Возникает проблема: объявление первого класса будет содержать ссылку на ещё не определённый класс. Она решается с помощью упреждающего объявления: type TPeople = class // Упреждающее объявление класса TPeople THuman = class Name: TPeople; ... end; type TPeople = class // Упреждающее объявление класса TPeople People: array of THuman; ... end; Первое объявление класса TPeople называется упреждающим. Оно необходимо для того, чтобы компилятор нормально воспринял объявление поля Name в классе THuman. Конструкторы и деструкторы Особой разновидностью методов являются конструкторы и деструкторы. Создание объекта включает выделение памяти под экземпляр и инициализацию его полей, а разрушение - очистку полей и освобождение памяти. Действия по инициализации и очистке полей специфичны для каждого конкретного класса объектов. По этой причине язык Delphi позволяет переопределить стандартный конструктор Create и стандартный деструктор Destroy для выполнения любых полезных действий. Можно даже определить несколько конструкторов и деструкторов (имена им назначает сам программист), чтобы обеспечить различные процедуры создания и разрушения объектов. Объявление конструкторов и деструкторов похоже на объявление обычных методов с той лишь разницей, что вместо зарезервированных слов function и procedure используются слова constructor и destructor. Пример: type TPeople = class Name: string; Family: string; procedure GetName; procedure GetFamily; construcor Create; destrucot Destroy; end; Возможная реализация: procedure TPeople.Create; begin TPeople.Name := ' '; TPeople.Family := ' '; end; procedure TPeople.Destroy; begin //Пока ничего не делаем end; Если объект содержит встроенные объекты или другие динамические данные, то конструктор - это как раз то место, где их нужно создавать. Конструктор применяется к классу или к объекту. Конструктор создаёт новый объект только в том случае, если перед его именем указано имя класса. Если указать имя уже существующего объекта, он поведёт себя по-другому: не создаст новый объект, а только выполнит код, содержащийся в теле конструктора. Если он применяется к классу, People := TPeople.Create; то выполняется следующая последовательность действий:
begin Writeln('Введите имя человека'); Readln(People.Name); end; Внутри методов обращения к полям и другим методам выполняются как к обычным переменным и подпрограммам без уточнения экземпляра объекта. Такое упрощение достигается путём использования в пределах метода псевдопеременной Self (стандартный идентификатор). Этот дополнительный скрытый параметр необходим в тех случаях, когда вы создаёте несколько объектов одного класса, так что каждый раз, когда вы применяете метод к одному из объектов, он должен оперировать именно со своими данными и не влиять на своих объектов-"братьев". Физически Self представляет собой дополнительный неявный параметр, передаваемый в метод при вызове. Этот параметр и указывает экземпляр объекта, к которому данный метод применяется. Практика показывает, что псевдопеременная Self редко используется в явном виде. Её необходимо применять только тогда, когда при написании метода может возникнуть какая-либо двусмысленность для компилятора. Разграничение доступа к атрибутам объектов Класс может иметь любое количество полей данных и методов. Однако объектно-ориентированный подход требует, чтобы данные были скрыты, или инкапсулированы, внутри использующего их класса. Использование методов для доступа к внутренним данным объекта уменьшает шансы появления ошибок, так как методы могут осуществить проверку правильности вводимых данных и не допустить ввода неверного значения. Инкапсуляция также важна, поскольку позволяет легко вносить изменения во внутреннюю структуру класса, не меняя его внешнее представление. Таким образом, автору класса несложно вносить изменения и модифицировать класс при переходе к следующей версии. Концепция инкапсуляции весьма проста: нужно просто думать о классе как о "чёрном ящике" с очень маленькой видимой частью. Видимая часть, которая называется интерфейсом класса, позволяет остальным частям программы осуществлять доступ к объектам этого класса и использовать их. Однако, когда вы работаете с объектами, большая часть их кода скрыта от вас. Вы, как правило, не знаете, как устроены внутренние данные объекта, и обычно у вас нет возможности прямого доступа к ним. При этом предполагается, что для доступа к данным вы будете использовать методы. Это объектно-ориентированный подход к классической программной концепции, называемой скрытием информации. Существует 4 спецификатора доступа: private, protected, public и published:
FName: string; procedure GetName; construcor Create; Destructor Destroy; property Name: string read FName write GetName; // Свойство end; Ключевые слова read и write называются спецификаторами доступа. После слова read указывается поле или метод, к которому происходит обращение при чтении (получении) значения свойства, а после write - поле или метод, к которому происходит обращение при записи (установке) значения свойства. Чтобы имена свойств не совпадали с именами полей, последние принято писать с буквы F. Обращение к свойствам выглядит в программе как обращение к полям: var People: TPeople; Get: string; ... People.Name := 'Сергей'; Get := People.Name; Если один из спецификаторов доступа опущен, то значение свойства можно либо только читать (задан спецификатор read), либо только записывать (задан спецификатор write). Следующий пример объявлено свойство, значение которого можно только читать: type TPeople = class FName: array of string; function GetName: integer; property Name: integer read GetName; end; function TPeople.GetName: integer; begin Result := Length(FName); end; Здесь свойство Name показывает количество элементов в массиве FName. Поскольку оно определяется в результате чтения, пользователю объекта разрешено только количество элементов. В отличии от полей свойства не имеют адреса в памяти, поэтому к ним запрещено применять операцию @. Как следствие, их нельзя передавать в var- и out-параметрах процедур и функций. Технология объектно-ориентированного программирования в среде Delphi предписывает избегать прямого обращения к полям, создавая вместо этого соответствующие свойства. Это упорядочивает работу с объектами, изолирую их данные от непосредственной модификации. В будущем внутренняя структура класса, которая иногда является достаточно сложной, может быть изменена с целью повышения эффективности работы программы. При этом потребуется переработать только методы чтения и записи значения свойств; внешний интерфейс класса не изменится. Методы получения (чтения) и установки (записи) значений свойств подчиняются определенным правилам. Метод чтения свойства - это всегда функция, возвращающая значение того же типа, что и тип свойства. Метод записи свойства - это обязательно процедура, принимающая параметр того же типа, что и тип свойства. В остальных отношениях это обычные методы объекта. Пример методов чтения и записи: type TPeople = class FName: boolean; procedure SetName(const AName: boolean); function GetName: integer; property Name: boolean read FName write SetName; property Count: integer read GetName; end; Использование методов для получения и установки свойств позволяет проверить корректность значения свойства, сделать дополнительные вычисления, установить значения зависимых полей и так далее. procedure TPeople.SetName(const AName: boolean); begin if Name <> AName then begin if ANfme then // Если состояние изменяется то ... else ... FName := AName; // Сохранение состояния в поле end; end; Один и тот же метод может использоваться для получения (установки) значений нескольких свойств одного типа. В этом случае каждому свойству назначается целочисленный индекс, который передаётся в метод чтения (записи) первым параметром. Пример: type TPeople = class ... property FirstName: string index 0 read GetItem; property LastName: string index 1 read GetItem; property Phone: string index 2 read GetItem ; end; var People: TPeople; ... Writeln(People.FirstName); // Эквивалентно: Writeln(People.GetItem(0)); Writeln(People.LastName); // Эквивалентно: Writeln(People.GetItem(1)); Writeln(People.Phone); // Эквивалентно: Writeln(People.GetItem(2)); ... Кроме обычных свойств в объектах существуют свойства-массивы. Свойство-массив - это индексированное множество значений. Пример: type TPeople = class FName: array of string; function GetName(index: integer): string; property Name[index: integer]: string read GetName; end; function TPeople.GetName(index: integer): string; begin Result := FName[index]; end; В описании свойства-массива разрешено использовать только методы, но не поля. В этом состоит отличие свойства-массива от обычного свойства. Основная выгода от применения свойства-массива - возможность выполнения операций с помощью цикла for, например: var People: TPeople; i: integer; ... for i:=0 to People.ItemCount - 1 do Writeln(People.Items[i]); ... Свойство-массив может быть многомерным. В этом случае методы чтения и записи элементов должны иметь столько же индексных параметров соответствующих типов, что и свойство-массив. Свойства-массивы имеют два важных отличия от обычных массивов:
type TPeople = class ... property Name(index: integer): string read GetName; default; ... end; Такое объявление свойства Name позволяет рассматривать сам объект класса TPeople как массив и опускать имя свойства-массива при обращении к нему из программы, например: var r: TPeople; i: integer; ... for I := 0 to R.ItemCount - 1 do Writeln(R[I]); Следует помнить, что только свойства-массивы могут быть основными свойствами объектов; для обычных свойств это недопустимо. По умолчанию Delphi самостоятельно сохраняет значения всех публикуемых свойств. Но программист имеет возможность управлять этим процессом. Для этого в составе строки, описывающей свойства, используется команда stored: property Name: string read FName write FName stored False; // отказ от запоминания Три кита объектно-ориентированного программирования Весь мир ООП держится на трех китах: инкапсуляции, наследовании и полиморфизме. Для начала о них надо иметь только самое общее представление. Объединение данных и операций в одну сущность - объект - тесно связано с понятием инкапсуляции, которое означает сокрытие внутреннего устройства. Инкапсуляция делает объекты похожими на маленькие программные модули, в которых скрыты внутренние данные и у которых имеется интерфейс использования в виде подпрограмм. Переход от понятий «структура данных» и «алгоритм» к понятию «объект» значительно повысил ясность и надежность программ. Второй кит ООП - наследование. Этот простой принцип означает, что если вы хотите создать новый класс объектов, который расширяет возможности уже существующего класса, то нет необходимости в переписывании заново всех полей, методов и свойств. Вы объявляете, что новый класс является потомком (или дочерним классом) имеющегося класса объектов, называемого предком (или родительским классом), и добавляете к нему новые поля, методы и свойства. Процесс порождения новых классов на основе других классов называется наследованием. Новые классы объектов имеют как унаследованные признаки, так и, возможно, новые. Очень важно, что в отношениях наследования любой класс может иметь только одного непосредственного предка и сколь угодно много потомков. Поэтому все связанные отношением наследования классы образуют иерархию. Примером иерархии классов является библиотека VCL; с ее помощью в среде Delphi обеспечивается разработка GUI-приложений. В языке Delphi существует предопределенный класс TObject, который служит неявным предком тех классов, для которых предок не указан. Это означает, что объявление type TPeople = class ... end; эквивалентно следующему: type TPeople = class(TObject) ... end; Класс TObject выступает корнем любой иерархии классов. Он содержит ряд методов, которые по наследству передаются всем остальным классам. Среди них конструктор Create, деструктор Destroy, метод Free и некоторые другие методы. Поскольку класс 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:
Для классов, связанных отношением наследования, вводится новое правило совместимости типов. Вместо объекта базового класса можно подставить объект любого производного класса. Обратное неверно. Правило совместимости классов чаще всего применяется при передаче параметров в параметрах процедур и функций. Все объекты являются представителями класса TObject. Поэтому любой объект любого класса можно использовать как объект класса TObject. Поскольку реальный экземпляр объекта может оказаться наследником класса, указанного при описании объектной переменной или параметра, бывает необходимо проверить, к какому классу принадлежит объект на самом деле. Чтобы программист мог выполнять такого рода проверки, каждый объект хранит информацию о своём классе. В языке Delphi существуют операторы is и as, с помощью которых выполняется проверка на тип и преобразование к типу. Пример проверки на принадлежность объекта Obj к классу TPeople или его наследнику: var Obj: TObject; ... if Obj is TPeople then ... Для преобразования объекта к нужному типу используется оператор as: with Obj as TPeople do // Равносильно: with TPeople(Obj) do Active := False; Вариант с оператором as лучше, поскольку безопасен. Он генерирует ошибку (точнее исключительную ситуацию) при выполнении программы, если реальный экземпляр объекта Obj не совместим с классом TPeople. Третий кит - это полиморфизм. Он означает, что в производных классах вы можете изменять работу уже существующих в базовом классе методов. При этом весь программный код, управляющий объектами родительского класса, пригоден для управления объектами дочернего класса без всякой модификации. Виртуальные методы Все методы, которые до сих пор рассматривались, являются статическими. Особенность такого метода заключается в его адресации. Она осуществляется ещё на стадии компиляции и компоновки проекта и будет неизменна (статична) до момента новой компиляции. Статическое связывание обладает существенным преимуществом над всеми остальными видами адресеции, поскольку обеспечивает самую высокую скорость вызова. Недостатком же фиксированной адресации является то, что статические методы не подлежат изменениям в классах-потомках. При обращении к статическому методу компилятор точно знает класс, к которому данный метод принадлежит. Объявление виртуального метода в базовом классе выполняется с помощью ключевого слова virtual, а его перекрытие в производных классах - с помощью ключевого слова override. Перекрытый метод должен иметь точно такой же формат (список параметров, а для функций ещё и тип возвращаемого значения), что и перекрываемый: type TPeople = class Name: string; procedure GetName; virtual; // Виртуальный метод end; type TStudent = class(TPeople) ... procedure GetName; override; end; Суть виртуальных методов в том, что они вызываются по фактическому типу экземпляра, а не по формальному типу, записанному в программе. Работа виртуальных методов основана на механизме позднего связывания. В отличие от раннего связывания, характерного для статических методов, позднее связывание основано на вычислении адреса вызываемого метода при выполнении программы. Адрес метода вычисляется по хранящемуся в каждом объекте описателю класса. Благодаря механизму наследования и виртуальных методов в среде Delphi реализуется такая концепция объектно-ориентированного программирования как полиморфизм. Полиморфизм существенно облегчает труд программистов, поскольку обеспечивает повторное использование кода уже написанных и отлаженных методов. Работа виртуальных методов основана на косвенном вызове подпрограмм. При косвенном вызове команда вызова подпрограммы оперирует не адресом подпрограммы, а адресом места в памяти, где хранится адрес подпрограммы. Для каждого виртуального метода создаётся процедурная переменная, но её наличие и использование скрыто от программиста. Все процедурные переменные с адресами виртуальных методов пронумерованы и хранятся в таблице, называемой таблицей виртуальных методов. Такая таблица создаётся одна для каждого класса объектов, и все объекты этого класса хранят на неё ссылку. Вызов виртуального метода происходит следующим образом: 1. Через объектную переменную выполняется обращение к занятому объектом блоку памяти. 2. Далее из этого блока извлекается адрес таблицы виртуальных методов (он записан в четырёх первых байтах). 3. На основании порядкового номера виртуального метода извлекается адрес соответствующей подпрограммы. 4. Вызывается код, находящийся по этому адресу. При построении иерархии классов часто возникает ситуация, когда работа виртуального метода в базовом классе не известна и наполняется содержанием только в наследниках. Директива abstract записывается после слова virtual и исключает необходимость написания кода виртуального метода для данного класса. Такой метод называется абстрактным, то есть подразумевает логическое действие, а не конкретный способ его реализации. Абстрактные виртуальные методы часто используются при создании полу готовых классов. Свою реализацию такие методы получают в законченных наследниках. Разновидностью виртуальных методов являются динамические методы. При их объявлении вместо ключевого слова virtual записывается ключевое слово dynamic. Динамические методы перечислены в специальном списке отдельно от таблицы виртуальных методов. В список динамических методов конкретного класса включены только адреса методов, описанных в данном классе. Поиск необходимого метода производится в обратном порядке дерева наследования. Если метод не найден в самом последнем дочернем классе, то поиск продолжается в его предке и так далее до TObject. В наследниках динамически методы перекрываются также, как и виртуальные - с помощью зарезервированного слова override. Если вы по какой-либо причине забудете указать директиву override, то унаследованный метод будет скрыт (но не отменён). Если вы решили спрятать предварительно объявленный метод, то поможет в этом директива reintroduce. Эта директива подавляет сообщения компилятора относительно уже существующего одноименного виртуального (динамического) метода в классе-предке. Задача команды inherited - принудительный вызов унаследованного от предка метода из метода, переопределяемого потомком (вызов исходного метода родительского класса). По смыслу динамические и виртуальные методы идентичны. Различие состоит только в механизме их вызова. Виртуальные методы вызываются максимально быстро, но платой за это является большой размер системных таблиц, с помощью которых определяются их адреса. Размер этих таблиц начинает сказываться с увеличением числа классов в иерархии. Динамические методы вызываются несколько дольше, но при этом таблицы с адресами методов имеют более компактный вид, что способствует экономии памяти. Специализированной формой динамических методов являются методы обработки сообщений. Они объявляются с помощью ключевого слова message, за которым следует целочисленная константа - номер сообщения. Пример из библиотеки VCL: type TWidgetControl = class(TControl) ... procedure CMKeyDown (var Msg: TCMKeyDown); message CM_KeyDown; ... end; Метод обработки сообщений имеет формат процедуры и содержит единственный var-параметр. При перекрытии такого метода название метода и имя параметра могут быть любыми, важно лишь, чтобы неизменным остался номер сообщения, используемый для вызова метода. Вызов метода выполняется не по имени, как обычно, а с помощью обращения к специальному методу Dispath, который имеется в каждом классе. Методы обработки сообщений применяются внутри библиотеки VCL для обработки команд пользовательского интерфейса и редко нужны при написании прикладных программ. Работа с файлами в Delphi. Коммерческий эффект от использования СУБД SQLBase компании Gupta. Воспроизведение звуков на Delphi. Новая версия FastScript 1.98. DBTreeView своими руками. Главная » Delphi |
© 2024 Team.Furia.Ru.
Частичное копирование материалов разрешено. |