Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

04 Лекция - Наследование

.pdf
Скачиваний:
53
Добавлен:
13.04.2015
Размер:
578.51 Кб
Скачать

Лекция № 4.

 

Тема: Наследование и полиморфизм.

 

Оглавление

 

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

1

Наследование и совместимость типов ........................................................

3

Позднее связывание и полиморфизм ..........................................................

3

Подмена и переопределение методов .........................................................

5

Виртуальные и динамические методы........................................................

6

Обработчики сообщений..............................................................................

7

Абстрактные методы ....................................................................................

7

Безопасное приведение типов......................................................................

8

Использование интерфейсов........................................................................

9

Ссылки класса .............................................................................................

12

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

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

Чтобы унаследовать в Delphi существующий класс, необходимо лишь указать на этот класс в начале определения нового класса. Например, это осуществляется каждый раз при создании новой формы:

type

TForml = class(TForm) end;

Это определение указывает, что класс TForm1 наследует все методы, поля, свойства и события класса TForm. Для объекта типа TForm можно вызвать любой опубликованный метод класса TForm. TForm, в свою очередь, наследует некоторые методы от другого класса, и т.д. до базового класса TОbject.

Напомним, что Tоbject является базовым, самым общим для всех остальных классом, предком по-умолчанию.

При описании нового класса указываются только дополнительные особенности по сравнению с классом–предком. Описанные ранее конструктор Create, деструктор Destroy и метод Free — это методы класса TObject, и они достались нам по наследству. Сам класс TObject, среди прочего, включает описания:

TObject = class

Constructor Create procedure Free;

destructor Destroy; virtual; end;

Для демонстрации явного наследования создадим новый собственный класс TCircle — класс окружностей. С точки зрения математики это покажется странным, но в ООП удобно определить окружность как точку с радиусом. Координаты этой точки координаты центра окружности, и окружность однозначно определяется положением центра и радиусом R. Вот как выглядит объявление этого класса

TCircle = class(TPoint) Fr: real;

procedure SetR(r: real); function GetR: real; end;

Из примера ясно, что класс–предок указывается в круглых скобках после ключевого слова class. В Pascal у каждого класса только один непосредственный предок, хотя наследование происходит и от предков предка. Весьма существенно, что механизм наследования позволил нам определить более сложный класс еще проще, чем простой. Ведь мы должны указать только изменения. Разумеется, дополнительные методы необходимо реализовать.

var

cl, c2: TCircle;

xl, yl, x2, y2, rl, r2: real; begin

cl := TCircle.Create; {Создание и инициализация объектов} cl.SetX(xl);

cl.SetY(yl);

cl.SetR(rl);

c2 := TCircle.Create; c2.SetX(x2); c2.SetY(y2); c2.SetR(r2);

cl.Free; cl := Nil; {Уничтожение объектов} c2.Free; c2 := Nil;

Отметим два факта. Мы обращались к методам объектов c1 и c2, которые описаны только в классе TPoint. Все это разрешается, ведь согласно нашему определению, “окружность — это точка …”. Каждый класс в Pascal может порождать несколько дочерних классов. Например, можно определить класс TArrow — стрелок, выходящих из некоторых точек. Снова, стрелка — это точка (положение начала), направление и размер.

TArrow = class(TPoint) Fphi, FLen: real;

procedure SetPhi(phi: real); function GetPhi: real; procedure SetLength(len: real) function GetLength: real; end;

Направление задается углом а координаты точки соответствуют координатам начала стрелки.

Наследование и совместимость типов

Pascal является строго типизированным языком. Это означает, что нельзя, например, присвоить целое значение логической переменной, кроме как с помощью явного приведения типов. Здесь существует правило: две переменные считаются совместимыми, если они имеют один тип данных или (что будет более точным) если их типы данных ссылаются на одно и то же определение типа. Для облегчения жизни программистов Delphi делает совместимыми некоторые предопределенные типы данных: допускается присвоение Extended значения Double и наоборот, с автоматическим повышением и понижением (с потенциальной потерей точности).

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

Существует важное исключение из этого правила в отношении типов класса. При объявлении класса, например, TAnimal (животное), и создании от него нового дочернего класса, допустим ТDog (собака), в дальнейшем можно назначить объект типа TDog переменной типа ТAnimal. Это допустимо, поскольку собака — это животное. В соответствии с общим правилом всегда можно использовать объект класса потомка, когда ожидается использование объекта класса предка. Но не наоборот! Нельзя использовать объект класса предка, когда ожидается использование объекта класса-потомка. Для облегчения восприятия повторим то же самое, но в виде программного кода:

var

MyAnimal: TAnimal; MyDog: TDog; begin

MyAnimal := MyDog; // Допускается MyDog := MyAnimal; // Это ошибка!!!

End;

Позднее связывание и полиморфизм

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

Эта методика называется полиморфизмом. Полиморфизм означает, что метод может вызываться применительно к переменной, но какой метод будет вызван, определяется средой Delphi в зависимости от типа объекта, на который ссылается переменная. Учитывая правила совместимости типов,

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

Например, предположим, что класс и его класс-потомок (пускай будут TAnimal и TDog) оба определяют одинаковый метод, и этот метод использует позднее связывание. Данный метод можно применить к общей переменной, например, MyAnimal, которая в ходе выполнения может обращаться как к объекту класса TAnimal, так и к объекту класса TDog. Какой метод действительно будет вызван — определяется в ходе выполнения в зависимости от класса текущего объекта.

Например, классы TAnimal и TDog имеют метод Voice, который воспроизводит звук, издаваемый выбранным животным. Метод Voice определен в классе TAnimal как виртуальный (с использованием ключевого слова virtual), а позже при определении TDog он подменяется (с использованием ключевого слова override):

type

TAnimal = class public

function Voice: string; virtual; TDog = class (TAnimal)

public

function Voice: string; override;

Эффект вызова MyAnimal.Voice будет различным. Если переменная MyAnimal в текущий момент ссылается на объект класса TAnimal, то будет вызван Tanimal.Voice. Если она ссылается на объект класса ТDog, то уже будет вызван TDog.Voice. Это происходит лишь благодаря тому, что функция является виртуальной.

Вызов MyAnimal.Voice будет работать для объекта, который является экземпляром любого из производных от TAnimal класса, даже для классов, определенных в других модулях или еще вовсе не написанных. Компилятор может и не знать обо всех потомках для того, чтобы сделать вызов, совместимый с ним; требуется лишь класс-предок. Другим словами, вызов MyAnimal.Voice совместим со всеми будущими классами-потомками класса

TAnimal.

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

Подмена и переопределение методов

Как вы только что видели, для того чтобы подменить метод с поздним связыванием в классе-потомке, необходимо использовать ключевое слово override (подменить). Обратите внимание, что это имеет место только в случае, когда метод был определен в классе-предке как виртуальный (или динамический) (virtual или dynamic). С другой стороны, если это статический метод, то отсутствует возможность активизировать позднее связывание; это возможно только с помощью изменения программного кода класса-предка.

Правила просты: метод, определенный как статический, остается статическим во всех производных классах, если только вы не «спрячете» его с помощью нового виртуального метода, имеющего то же самое имя. Метод, определенный как виртуальный, остается методом, поддерживающим позднее (динамическое) связывание, во всех производных классах (если только вы не «спрячете» его с помощью нового статического метода, выполняющего совершенно пустое действие). Это поведение ничем не изменить ввиду того, что для динамически связываемых методов компилятор генерирует различный код. Для переопределения статического метода в классе-наследнике необходимо добавить метод, имеющий те же или другие параметры, не указывая дальнейших спецификаций. Для подмены виртуального метода необходимо указать те же параметры и использовать ключевое слово override:

type

TAnimal = class public

function Voice: string; virtual; end;

type

TMyClass = class procedure One; virtual;

procedure Two; {static method} end;

TMyDerivedClass = class (MyClass) procedure One; override; procedure Two;

end;

Как правило, подменить метод можно двумя способами: заменить метод класса-предка новой версией, либо добавить дополнительный программный код в существующий метод. Это можно выполнить с помощью ключевого слова inherited при вызове того же метода класса-предка. Например, можно написать:

procedure TMyDerivedClass.One; begin

//новый программный код

...

//вызов унаследованной процедуры MyClass.One; inherited One;

end;

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

классе. Если именно это и надо, то можно выполнить переопределенный метод (метод классародителя), пометив его ключевым словом overload (перегрузка). Если этот метод имеет параметры, отличающиеся от параметров базового класса, то он действительно становится перегруженным методом; в противном случае он заменит метод базового класса. Обратите внимание, что в базовом классе этот метод не должен помечаться как overload, однако если в базовом классе метод является виртуальным, то компилятор выдаст предупреждение: «Method 'One' hides virtual method of base type 'TMyClass'» (Метод 'One' прячет виртуальный метод базового типа 'TMyClass'). Для того чтобы избежать появления такого сообщения и известить компилятор о ваших намерениях, можно использовать директиву reintroduce.

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

type

TMyClass = class procedure One; end;

TMyDerivedClass = class (TMyClass) procedure One (S: string);

end;

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

В Delphi существует два способа активизации позднего связывания. Можно объявить метод как виртуальный, или объявить его динамическим. Синтаксис использования ключевых слов virtual и dynamic абсолютно одинаковый и результат их использования тоже одинаковый. Различным является л ишь внутренний механизм, используемый компилятором для реализации позднего связывания.

Виртуальные методы основываются на таблице виртуальных методов (virtual method table, VMT, иногда используется наименование viable), которая представляет собой массив адресов методов. Для вызова виртуального метода компилятор генерирует код перехода на адрес, хранимый в n-й ячейке таблицы виртуальных методов объекта. VMT-таблицы обеспечивают быстрое выполнение вызовов методов, но они требуют наличия элемента таблицы для каждого виртуального метода каждого класса-потомка, даже если в наследуемом классе метод не подменяется.

С другой стороны, динамические (Dynamic) вызовы методов координируются с помощью уникального числа, указывающего на метод, который хранится в классе только в том случае, если класс определяет или подменяет его. Поиск соответствующей функции становится более длительным по сравнению с таблицей соответствия виртуальных методов. Преимущество заключается в том, что элементы, соответствующие

динамическим методам, распространяются в потомках, только если потомки подменяют данный метод.

Обработчики сообщений

Метод, осуществляющий позднее связывание, может также использоваться для обработки сообщений Windows, хотя методика несколько отличается. Для этой цели Delphi предоставляет еще одну директиву, message, предназначенную для идентификации методов, обрабатывающих сообщения, которые должны представлять из себя процедуру с единственным параметром var. Директива message сопровождается номером Windows-сообщения, которое данный метод будет обрабатывать.

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

type

TForml = class(TForm)

procedure WMUser (var Msg: TMessage); message wm User; end;

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

PostMessage (Form1.Handle, wm User, 0, 0);

Эта методика может быть чрезвычайно полезна для бывалых программистов, которые знают все о сообщениях Windows и API-функциях. Вы можете также немедленно посылать сообщение, вызывая APIфункцию

SendMessage или VCL-метод Perform.

Абстрактные методы

Ключевое слово abstract используется для объявления методов, которые будут определены только в классах-потомках текущего класса. Директива abstract полностью определяет метод. Если вы попробуете предоставить уточнение (формулировку) этого метода, то компилятор выразит недовольство. В Delphi можно создавать экземпляры классов, имеющие абстрактные методы. Однако при этом 32-разряд- ный компилятор Delphi выдает предупреждающее сообщение «Constructing instance of <class name> containing abstract methods» (Создание экземпляра <имя класса>, содержащего абстрактные методы). Если в ходе выполнения будет вызван абстрактный метод, Delphi вызовет исключение:

type

TAnimal = class public

function Voice: string; virtual; abstract;

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

Вы можете поинтересоваться, а зачем использовать абстрактные методы? Причина заключается в поддержке полиморфизма. Если класс TAnimal имеет виртуальный метод Voice, то каждый производный от него класс может переопределить его. Если он имеет абстрактный метод Voice, то каждый производный от него класс должен переопределить его.

Безопасное приведение типов

Правило совместимости типов Delphi для классов-наследников позволяет использовать класс-наследник там, где ожидается использование класса-родителя. Как упоминалось ранее, обратный вариант невозможен. Теперь давайте представим, что класс TDog имеет метод Eat, который не представлен в базовом классе TAnimal. Если переменная MyAnimal обращается к собаке, она сможет вызвать эту функцию, но если переменная относится к другому классу, то попытка вызова приведет к ошибке. Явное приведение типов может привести к неприятной ошибке времени выполнения (или, еще хуже, проблеме наложения при записи в памяти), поскольку компилятор не сможет определить, является ли верным тип объекта и действительно ли существует вызываемый метод.

Для решения этой проблемы можно использовать методики, основанные на технологии «информация о типах в процессе исполнения» (run-time type information, сокращенно RTTI). По существу, поскольку каждый объект «знает» свой тип и свой родительский класс, можно получить эту информацию с помощью оператора is (либо, в особых случаях — с помощью метода InheritsFrom класса TObject). Параметры оператора is — объект и тип класса, а возвращаемое значение имеет логический тип:

if MyAnimal is TDog then ...

Выражение is будет иметь значение True, только если объект MyAnimal в настоящее время относится к объекту класса TDog, либо к классу, исходящему от TDog. Это означает, что при проверке является ли объект TDog типом TAnimal, результат будет положительным. Иначе говоря, это выражение равно True, если можно безопасно присвоить данный объект (MyAnimal) переменной этого типа данных (TDog). Теперь, когда вы уверены, что данным животным является собака, можно выполнить безопасное приведение типов (преобразование). Явное приведение можно выполнить с помощью следующего программного кода:

var

MyDog: TDog; begin

if MyAnimal is TDog then begin

MyDog := TDog (MyAnimal); Text := MyDog.Eat;

end;

Это же действие может быть выполнено непосредственно вторым RTTIоператором as, который преобразует тип объекта только в том случае, если запрашиваемый класс совместим с текущим. Параметрами оператора as

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

MyDog := MyAnimal as TDog; Text := MyDog.Eat;

Если необходимо лишь вызвать функцию Eat, также можно использовать и более короткую запись:

(MyAnimal as TDog).Eat;

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

Для того чтобы избежать этого исключения, используйте оператор is и при положительном результате выполняйте прямое приведение (фактически, нет причины последовательно использовать is и as, что приводит к двойной проверке типа):

if MyAnimal is TDog then TDog(MyAnimal).Eat;

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

procedure TForm1.Button1Click(Sender: TObject); begin

if Sender is TButton then

end;

В Delphi это довольно обычная практика. Два RTTIоператора, is и as, чрезвычайно мощны, но необходимо ограничивать их использование только в исключительных случаях. При необходимости решить сложную проблему, включающую несколько классов, пробуйте сначала использовать полиморфизм. Только в отдельных случаях, где один полиморфизм не справляется, можно пробовать использовать RTTIоператоры. Не используйте RTTI вместо полиморфизма. Это неправильная практика программирования, которая ведет к снижению скорости выполнения программ. RTTI-операторы отрицательно влияют на производительность, поскольку для того, чтобы увидеть, является ли приведение типов корректным, им приходится пройти по всей иерархии классов. Как вы уже видели, вызовы виртуального метода требуют лишь поиска в памяти, что осуществляется гораздо быстрее.

RTTI-информация — это не только операторы as и is. С ее помощью в ходе выполнения можно узнать подробные сведения о классе и типе, особенно для свойств, событий и методов, объявленных как published.

Использование интерфейсов

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

класс является настолько абстрактным, что он лишь перечисляет ряд виртуальных функций, не обеспечивая их реализацию. Этот вид «чисто» абстрактного класса также может быть определен с помощью специальной технологии, именуемой interface (интерфейс). Ввиду этого мы обращаемся к этим классам как к интерфейсам.

Технически интерфейс — это не класс, хотя и напоминает его. От класса его отличает то, то он воспринимается как полностью отдельный элемент с определенными характеристиками:

в отношении объектов типа interface ведется подсчет числа ссылок, и интерфейсы автоматически уничтожаются при отсутствии ссылок на них. Этот механизм подобен тому, как Delphi управляет длинными строками; это делает управление памятью практически автоматизированным;

класс может исходить от единственного базового класса, но может реализовывать множество интерфейсов;

так же, как все классы исходят от TObject, все интерфейсы исходят от iInterface, формируя полностью отдельную иерархию.

Важно подчеркнуть, что интерфейсы поддерживают несколько иную, чем классы, модель ООП. Интерфейсы обеспечивают менее ограниченную реализацию полиморфизма. Полиморфизм объектной ссылки основан на определенной ветви иерархии. Полиморфизм интерфейса работает по всей иерархии. Безусловно, интерфейсы придерживаются идеи инкапсуляции и обеспечивают более свободное соединение между классами, чем наследование. Обратите внимание, что самые современные ООП-языки, от Java до С#, имеют понятие интерфейсов.

Вот синтаксис объявления интерфейса (чье имя по существующим соглашениям начинается с буквы i):

type

ICanFly = interface

['{EAD9C4B4-E1C5-4CF4-9FA0-3B812C880A21}'] function Fly: string;

end;

Этот интерфейс имеет глобально уникальный идентификатор (Globally Unique Identifier, GUID) — числовой идентификатор, имеющий определение, и основанный на Windows-соглашениях. Эти идентификаторы можно генерировать в редакторе Delphi нажатием сочетания клавиш Ctrl+Shift+G.

Хотя интерфейс можно компилировать и использовать без указания GUID, последний обязательно придется сгенерировать, поскольку он требуется для запроса интерфейса или динамического приведения типов as с использованием типа этого интерфейса. Все преимущество интерфейсов заключается (обычно) в предоставлении гибкости во время выполнения; поэтому, в отличие от типов класса, интерфейсы без GUID не очень полезны.

После того как интерфейс объявлен, для его реализации можно определить класс:

type

TAirplane = class (TInterfacedObject, ICanFly) function Fly: string;

end;