Фундаментально про объектно-ориентированное программированиеИсточник: DelphiSources Федоренко Сергей
Введение Язык программирования Object Pascal и его достойный преемник, среда программирования Delphi, построены на основе получившей широкое развитие на стыке 70 - 80-х годов 20 века теории объектно-ориентированного программирования. В то время идея описания программ в базисе логических сущностей и взаимодействия между ними не была такой уж бесспорной, а у некоторых оппонентов даже вызывала определённое недоумение. Преимущества ООП по сравнению с традиционными способами программирования:
Объект и класс Еже само название концепции "объектно-ориентированное программирование" указывает на то, что ключевой фигурой в ООП является объект. Объекты - это крупнейшее достижение в современной технологии программирования. Они позволили строить программу не из чудовищных по сложности процедур и функций, а из кирпичиков-объектов, заранее наделённых нужными свойствами. Самое приятное в объектах это то, что их внутренняя сложность скрыта от программиста, который просто пользуется готовым строительным материалом. Все присущие объекту характеристики в терминах ООП называют полями. По сути, это принадлежащие объекту переменные определённого типа, в которые записываются значения, отражающие состояние объекта. Для управления объектом предназначены его методы. В первую очередь методы воздействуют на поля объекта. Если методы объекта возвращают какое-нибудь значение, то они реализуются в виде функций, в противном случае метод представляется процедурой. Объект не может возникнуть из воздуха, среда программирования каким-то образом должна быть проинформирована о его характеристиках. Поэтому предварительно программист описывает объект; такое описание называется классом. Класс - это чертёж будущего объекта, в котором учитываются не только его конструктивные элементы (поля), но и определяются способы управления этими элементами - методы класса. Определение класса начинается с ключевого слова type глобального блока, за которым следуют имя класса, его поля и методы. Завершается описание директивой end. В объявление класса могут входить другие классы; таким образом можно создавать сложные составные объекты. По форме объявления классы похожи на обычные записи, но помимо полей данных могут содержать объявления пользовательских процедур и функций (методы). Пример: type TPeople = class Name: string; Family: string; procedure GetName; procedure GetFamily; end; Класс содержит поля (Name, Family) и методы (GetName, GetFamily). Заголовки методов, всегда следующие за списком полей, играют роль упреждающих описаний. Программный код методов пишется отдельно от определения класса и будет приведён позже. var People: TPeople; При работе с обычными типами данных этого объявления было бы достаточно для получения экземпляра типа. Однако объекты среды Delphi являются динамическими данными, то есть распределяются в динамической памяти. Поэтому переменная People - это просто ссылка на экземпляр (объект в памяти), которого физически ещё не существует. Чтобы сконструировать объект (выделить память для экземпляра) класса TPeople и связать с ним переменную People, нужно в тексте программы поместить следующий оператор: People := TPeople.Create; //Выделение памяти под объект Create - это так называемый конструктор объекта; он всегда присутствует в классе и служит для создания и инициализации экземпляров. При создании объекта в памяти выделяется место только для его полей. Методы, как и обычные процедуры и функции, помещаются в область кода программы; они умеют работать с любыми экземплярами своего класса и не дублируются в памяти. После создания объект можно использовать в программе: получать и устанавливать значения его полей, вызывать его методы. Доступ к полям и методам объекта происходит с помощью уточнённых имён, например: People.GetName; Кроме того, как и при работе с записями, допустимо использование оператора with, например: with People do Если объект становится ненужным, он должен быть удалён вызова специального метода Destroy, например: People.Destroy; //Освобождение памяти, занимаемой объектом Destroy - это так называемый деструктор объекта; он присутствует в классе наряду с конструктором и служит для удаления объекта из динамической памяти. После вызова деструктора переменная People становится несвязанной и не должна использоваться для доступа к полям и методам уже несуществующего объекта. Чтобы отличать в программе связанные объектные переменные от несвязанных, последнее следует инициализировать значением nil. Пример: People := nil; Вызов деструктора для несуществующих объектов недопустим и при выполнении программы приведёт к ошибке. Чтобы избавить программистов от лишних ошибок, в объекты ввели предоставленный метод Free, который следует вызвать вместо деструктора. Метод Free сам вызывает деструктор Destroy, но только в том случае, если значение объектной переменной не равно nil. Поэтому последнюю строчку в приведённом выше примере можно переписать следующим образом: People.Free; После уничтожения объекта переменная People сохраняет своё значение, продолжая ссылаться на место в памяти, где объекта уже нет. Если эту переменную предполагается ещё использовать, то желательно присвоить ей значение nil, чтобы программа могла проверить, существует объект или нет. Таким образом, наиболее правильная последовательность действий при уничтожении объекта должна быть следующая: People. Free; С помощью стандартной процедуры FreeAndNil это можно сделать проще и элегантнее: FreeAndNil(People); //Эквивалентно: People. Free; People := nil; Значение одной объектной переменной можно присвоить другой. При этом объект не копируется в памяти, а вторая переменная просто связывается с тем же объектом, что и первая: var People1, People2: TPeople; Объекты могут выступать в программе не только в качестве переменных, но также элементов массивов, полей записей, параметров процедур и функций. Кроме того, они могут служить полями других объектов. Во всех этих случаях программист фактически оперирует указателями на экземпляры объектов в динамической памяти. Следовательно, объекты изначально приспособлены для создания сложных динамических структур данных, таких как списки и деревья. Указатели на объекты для этого не нужны. В некоторых случаях требуется, чтобы объекты разных классов содержали ссылки друг на друга. Возникает проблема: объявление первого класса будет содержать ссылку на ещё не определённый класс. Она решается с помощью упреждающего объявления: type TPeople = class // Упреждающее объявление класса TPeople type TPeople = class // Упреждающее объявление класса TPeople Первое объявление класса TPeople называется упреждающим. Оно необходимо для того, чтобы компилятор нормально воспринял объявление поля Name в классе THuman. Конструкторы и деструкторы Особой разновидностью методов являются конструкторы и деструкторы. Создание объекта включает выделение памяти под экземпляр и инициализацию его полей, а разрушение - очистку полей и освобождение памяти. Действия по инициализации и очистке полей специфичны для каждого конкретного класса объектов. По этой причине язык Delphi позволяет переопределить стандартный конструктор Create и стандартный деструктор Destroy для выполнения любых полезных действий. Можно даже определить несколько конструкторов и деструкторов (имена им назначает сам программист), чтобы обеспечить различные процедуры создания и разрушения объектов. Объявление конструкторов и деструкторов похоже на объявление обычных методов с той лишь разницей, что вместо зарезервированных слов function и procedure используются слова constructor и destructor. Пример: type TPeople = class Возможная реализация: procedure TPeople.Create; Если объект содержит встроенные объекты или другие динамические данные, то конструктор - это как раз то место, где их нужно создавать. Конструктор применяется к классу или к объекту. Конструктор создаёт новый объект только в том случае, если перед его именем указано имя класса. Если указать имя уже существующего объекта, он поведёт себя по-другому: не создаст новый объект, а только выполнит код, содержащийся в теле конструктора. Если он применяется к классу, People := TPeople.Create; то выполняется следующая последовательность действий:
Таким образом, хотя на первый взгляд синтаксис конструктора схож с вызовом процедуры (не определено возвращаемое значение), но на самом деле конструктор - это функция, возвращающая созданный и инициализированный объект. Если конструктор применяется к объекту, People.Create; то конструктор выполняется как обычный метод. Другими словами, новый объект не создаётся, а происходит повторная инициализация полей существующего объекта. В этом случае конструктор не возвращает никакого значения. Далеко не все объекты корректно себя ведут при повторной инициализации, поскольку программисты редко закладывают такую возможность в свои классы. Поэтому на практике повторная инициализация применяется крайне редко. Деструктор уничтожает объект к которому применяется: People.Destroy; В результате выполняются:
Методы Процедуры и функции, предназначенные для выполнения над объектами действий, называются методами. Предварительное объявление методов выполняется при описании класса в секции interface модуля, а их программный код записывается в секции implementation. Однако в отличии от обычных процедур и функций заголовки методов должны иметь уточнённые имена, то есть содержать наименование класса. Пример: procedure TPeople.GetName; Внутри методов обращения к полям и другим методам выполняются как к обычным переменным и подпрограммам без уточнения экземпляра объекта. Такое упрощение достигается путём использования в пределах метода псевдопеременной Self (стандартный идентификатор). Этот дополнительный скрытый параметр необходим в тех случаях, когда вы создаёте несколько объектов одного класса, так что каждый раз, когда вы применяете метод к одному из объектов, он должен оперировать именно со своими данными и не влиять на своих объектов-"братьев". Физически Self представляет собой дополнительный неявный параметр, передаваемый в метод при вызове. Этот параметр и указывает экземпляр объекта, к которому данный метод применяется. Практика показывает, что псевдопеременная Self редко используется в явном виде. Её необходимо применять только тогда, когда при написании метода может возникнуть какая-либо двусмысленность для компилятора. Разграничение доступа к атрибутам объектов Класс может иметь любое количество полей данных и методов. Однако объектно-ориентированный подход требует, чтобы данные были скрыты, или инкапсулированы, внутри использующего их класса. Использование методов для доступа к внутренним данным объекта уменьшает шансы появления ошибок, так как методы могут осуществить проверку правильности вводимых данных и не допустить ввода неверного значения. Инкапсуляция также важна, поскольку позволяет легко вносить изменения во внутреннюю структуру класса, не меняя его внешнее представление. Таким образом, автору класса несложно вносить изменения и модифицировать класс при переходе к следующей версии. Концепция инкапсуляции весьма проста: нужно просто думать о классе как о "чёрном ящике" с очень маленькой видимой частью. Видимая часть, которая называется интерфейсом класса, позволяет остальным частям программы осуществлять доступ к объектам этого класса и использовать их. Однако, когда вы работаете с объектами, большая часть их кода скрыта от вас. Вы, как правило, не знаете, как устроены внутренние данные объекта, и обычно у вас нет возможности прямого доступа к ним. При этом предполагается, что для доступа к данным вы будете использовать методы. Это объектно-ориентированный подход к классической программной концепции, называемой скрытием информации. Существует 4 спецификатора доступа: private, protected, public и published:
Перечисленные секции могут чередоваться в объявлении класса в произвольном порядке, однако в пределах секции сначала следует описание полей, а потом методов и свойств. Если в определении класса нет ключевых слов private, public, protected, published, то для обычных классов всем полям, методам и свойствам приписывается атрибут видимости public, а для тех классов, которые порождены от классов библиотеки VCL, - атрибут видимости published. Внутри модуля никакие ограничения на доступ к атрибутам классов, реализованных в том же модуле, не действуют. Свойства Помимо полей и методов в объектах существуют свойства. При работе с объектом свойства выглядят как поля: они принимают значения и участвуют в выражениях. Но в отличии от полей свойства не занимают место в памяти, а операции их чтения и записи ассоциируются с обычными полями и методами. Это позволяет создавать необходимые сопутствующие эффекты при обращении к свойствам. Объявление свойства выполняется с помощью зарезервированного слова property, например: type TPeople = class Ключевые слова read и write называются спецификаторами доступа. После слова read указывается поле или метод, к которому происходит обращение при чтении (получении) значения свойства, а после write - поле или метод, к которому происходит обращение при записи (установке) значения свойства. Чтобы имена свойств не совпадали с именами полей, последние принято писать с буквы F. Обращение к свойствам выглядит в программе как обращение к полям: var People: TPeople; Если один из спецификаторов доступа опущен, то значение свойства можно либо только читать (задан спецификатор read), либо только записывать (задан спецификатор write). Следующий пример объявлено свойство, значение которого можно только читать: type TPeople = class function TPeople.GetName: integer; Здесь свойство Name показывает количество элементов в массиве FName. Поскольку оно определяется в результате чтения, пользователю объекта разрешено только количество элементов. В отличии от полей свойства не имеют адреса в памяти, поэтому к ним запрещено применять операцию @. Как следствие, их нельзя передавать в var- и out-параметрах процедур и функций. Технология объектно-ориентированного программирования в среде Delphi предписывает избегать прямого обращения к полям, создавая вместо этого соответствующие свойства. Это упорядочивает работу с объектами, изолирую их данные от непосредственной модификации. В будущем внутренняя структура класса, которая иногда является достаточно сложной, может быть изменена с целью повышения эффективности работы программы. При этом потребуется переработать только методы чтения и записи значения свойств; внешний интерфейс класса не изменится. Методы получения (чтения) и установки (записи) значений свойств подчиняются определенным правилам. Метод чтения свойства - это всегда функция, возвращающая значение того же типа, что и тип свойства. Метод записи свойства - это обязательно процедура, принимающая параметр того же типа, что и тип свойства. В остальных отношениях это обычные методы объекта. Пример методов чтения и записи: type TPeople = class Использование методов для получения и установки свойств позволяет проверить корректность значения свойства, сделать дополнительные вычисления, установить значения зависимых полей и так далее. procedure TPeople.SetName(const AName: boolean); Один и тот же метод может использоваться для получения (установки) значений нескольких свойств одного типа. В этом случае каждому свойству назначается целочисленный индекс, который передаётся в метод чтения (записи) первым параметром. Пример: type TPeople = class var People: TPeople; Кроме обычных свойств в объектах существуют свойства-массивы. Свойство-массив - это индексированное множество значений. Пример: type TPeople = class В описании свойства-массива разрешено использовать только методы, но не поля. В этом состоит отличие свойства-массива от обычного свойства. Основная выгода от применения свойства-массива - возможность выполнения операций с помощью цикла for, например: var People: TPeople; Свойство-массив может быть многомерным. В этом случае методы чтения и записи элементов должны иметь столько же индексных параметров соответствующих типов, что и свойство-массив. Свойства-массивы имеют два важных отличия от обычных массивов:
Свойство-массив можно сделать основным свойством объектов данного класса. Для этого в описание свойства добавляется слово default: type TPeople = class Такое объявление свойства Name позволяет рассматривать сам объект класса TPeople как массив и опускать имя свойства-массива при обращении к нему из программы, например: var r: TPeople; Следует помнить, что только свойства-массивы могут быть основными свойствами объектов; для обычных свойств это недопустимо. По умолчанию Delphi самостоятельно сохраняет значения всех публикуемых свойств. Но программист имеет возможность управлять этим процессом. Для этого в составе строки, описывающей свойства, используется команда stored: property Name: string read FName write FName stored False; // отказ от запоминания Три кита объектно-ориентированного программирования Весь мир ООП держится на трех китах: инкапсуляции, наследовании и полиморфизме. Для начала о них надо иметь только самое общее представление. Объединение данных и операций в одну сущность - объект - тесно связано с понятием инкапсуляции, которое означает сокрытие внутреннего устройства. Инкапсуляция делает объекты похожими на маленькие программные модули, в которых скрыты внутренние данные и у которых имеется интерфейс использования в виде подпрограмм. Переход от понятий «структура данных» и «алгоритм» к понятию «объект» значительно повысил ясность и надежность программ. Второй кит ООП - наследование. Этот простой принцип означает, что если вы хотите создать новый класс объектов, который расширяет возможности уже существующего класса, то нет необходимости в переписывании заново всех полей, методов и свойств. Вы объявляете, что новый класс является потомком (или дочерним классом) имеющегося класса объектов, называемого предком (или родительским классом), и добавляете к нему новые поля, методы и свойства. Процесс порождения новых классов на основе других классов называется наследованием. Новые классы объектов имеют как унаследованные признаки, так и, возможно, новые. Очень важно, что в отношениях наследования любой класс может иметь только одного непосредственного предка и сколь угодно много потомков. Поэтому все связанные отношением наследования классы образуют иерархию. Примером иерархии классов является библиотека VCL; с ее помощью в среде Delphi обеспечивается разработка GUI-приложений. В языке Delphi существует предопределенный класс TObject, который служит неявным предком тех классов, для которых предок не указан. Это означает, что объявление type TPeople = class Класс TObject выступает корнем любой иерархии классов. Он содержит ряд методов, которые по наследству передаются всем остальным классам. Среди них конструктор Create, деструктор Destroy, метод Free и некоторые другие методы. Поскольку класс TObject является предком для всех других классов (в том числе и для ваших собственных), то не лишним будет кратко ознакомиться с его методами: type Краткое описание методов в классе TObject:
В механизме наследования можно условно выделить три основных момента:
Наследование свойств и методов имеет свои особенности. Свойство базового класса можно перекрыть в производном классе, например чтобы добавить ему новый атрибут доступа или связать с другим полем или методом. Метод базового класса тоже можно перекрыть в производном классе, например чтобы изменить логику его работы. Для классов, связанных отношением наследования, вводится новое правило совместимости типов. Вместо объекта базового класса можно подставить объект любого производного класса. Обратное неверно. Правило совместимости классов чаще всего применяется при передаче параметров в параметрах процедур и функций. Все объекты являются представителями класса TObject. Поэтому любой объект любого класса можно использовать как объект класса TObject. Поскольку реальный экземпляр объекта может оказаться наследником класса, указанного при описании объектной переменной или параметра, бывает необходимо проверить, к какому классу принадлежит объект на самом деле. Чтобы программист мог выполнять такого рода проверки, каждый объект хранит информацию о своём классе. В языке Delphi существуют операторы is и as, с помощью которых выполняется проверка на тип и преобразование к типу. Пример проверки на принадлежность объекта Obj к классу TPeople или его наследнику: var Obj: TObject; Для преобразования объекта к нужному типу используется оператор as: with Obj as TPeople do // Равносильно: with TPeople(Obj) do Вариант с оператором as лучше, поскольку безопасен. Он генерирует ошибку (точнее исключительную ситуацию) при выполнении программы, если реальный экземпляр объекта Obj не совместим с классом TPeople. Третий кит - это полиморфизм. Он означает, что в производных классах вы можете изменять работу уже существующих в базовом классе методов. При этом весь программный код, управляющий объектами родительского класса, пригоден для управления объектами дочернего класса без всякой модификации. Виртуальные методы Все методы, которые до сих пор рассматривались, являются статическими. Особенность такого метода заключается в его адресации. Она осуществляется ещё на стадии компиляции и компоновки проекта и будет неизменна (статична) до момента новой компиляции. Статическое связывание обладает существенным преимуществом над всеми остальными видами адресеции, поскольку обеспечивает самую высокую скорость вызова. Недостатком же фиксированной адресации является то, что статические методы не подлежат изменениям в классах-потомках. При обращении к статическому методу компилятор точно знает класс, к которому данный метод принадлежит. Объявление виртуального метода в базовом классе выполняется с помощью ключевого слова virtual, а его перекрытие в производных классах - с помощью ключевого слова override. Перекрытый метод должен иметь точно такой же формат (список параметров, а для функций ещё и тип возвращаемого значения), что и перекрываемый: type TPeople = class Суть виртуальных методов в том, что они вызываются по фактическому типу экземпляра, а не по формальному типу, записанному в программе. Работа виртуальных методов основана на механизме позднего связывания. В отличие от раннего связывания, характерного для статических методов, позднее связывание основано на вычислении адреса вызываемого метода при выполнении программы. Адрес метода вычисляется по хранящемуся в каждом объекте описателю класса. Благодаря механизму наследования и виртуальных методов в среде Delphi реализуется такая концепция объектно-ориентированного программирования как полиморфизм. Полиморфизм существенно облегчает труд программистов, поскольку обеспечивает повторное использование кода уже написанных и отлаженных методов. Работа виртуальных методов основана на косвенном вызове подпрограмм. При косвенном вызове команда вызова подпрограммы оперирует не адресом подпрограммы, а адресом места в памяти, где хранится адрес подпрограммы. Для каждого виртуального метода создаётся процедурная переменная, но её наличие и использование скрыто от программиста. Все процедурные переменные с адресами виртуальных методов пронумерованы и хранятся в таблице, называемой таблицей виртуальных методов. Такая таблица создаётся одна для каждого класса объектов, и все объекты этого класса хранят на неё ссылку. Вызов виртуального метода происходит следующим образом: При построении иерархии классов часто возникает ситуация, когда работа виртуального метода в базовом классе не известна и наполняется содержанием только в наследниках. Директива abstract записывается после слова virtual и исключает необходимость написания кода виртуального метода для данного класса. Такой метод называется абстрактным, то есть подразумевает логическое действие, а не конкретный способ его реализации. Абстрактные виртуальные методы часто используются при создании полу готовых классов. Свою реализацию такие методы получают в законченных наследниках. Разновидностью виртуальных методов являются динамические методы. При их объявлении вместо ключевого слова virtual записывается ключевое слово dynamic. Динамические методы перечислены в специальном списке отдельно от таблицы виртуальных методов. В список динамических методов конкретного класса включены только адреса методов, описанных в данном классе. Поиск необходимого метода производится в обратном порядке дерева наследования. Если метод не найден в самом последнем дочернем классе, то поиск продолжается в его предке и так далее до TObject. В наследниках динамически методы перекрываются также, как и виртуальные - с помощью зарезервированного слова override. Если вы по какой-либо причине забудете указать директиву override, то унаследованный метод будет скрыт (но не отменён). Если вы решили спрятать предварительно объявленный метод, то поможет в этом директива reintroduce. Эта директива подавляет сообщения компилятора относительно уже существующего одноименного виртуального (динамического) метода в классе-предке. Задача команды inherited - принудительный вызов унаследованного от предка метода из метода, переопределяемого потомком (вызов исходного метода родительского класса). По смыслу динамические и виртуальные методы идентичны. Различие состоит только в механизме их вызова. Виртуальные методы вызываются максимально быстро, но платой за это является большой размер системных таблиц, с помощью которых определяются их адреса. Размер этих таблиц начинает сказываться с увеличением числа классов в иерархии. Динамические методы вызываются несколько дольше, но при этом таблицы с адресами методов имеют более компактный вид, что способствует экономии памяти. Специализированной формой динамических методов являются методы обработки сообщений. Они объявляются с помощью ключевого слова message, за которым следует целочисленная константа - номер сообщения. Пример из библиотеки VCL: type TWidgetControl = class(TControl) Метод обработки сообщений имеет формат процедуры и содержит единственный var-параметр. При перекрытии такого метода название метода и имя параметра могут быть любыми, важно лишь, чтобы неизменным остался номер сообщения, используемый для вызова метода. Вызов метода выполняется не по имени, как обычно, а с помощью обращения к специальному методу Dispath, который имеется в каждом классе. Методы обработки сообщений применяются внутри библиотеки VCL для обработки команд пользовательского интерфейса и редко нужны при написании прикладных программ. |