Наследование и переопределение.
При построении нового объектного типа можно использовать некоторый ранее определенный тип. Пусть, например, необходимо построить объектный тип, управляющий простой геометрической фигурой - кругом - на экране дисплея. Структура информации для определения круга очень похожа на структуру для точки: здесь также необходимы поля Х и Y для фиксации центра круга и логическое поле Visible для определения видимости круга в текущий момент. Нужно еще добавить поле для хранения радиуса круга.
Традиционный стиль программирования допускает два решения. Во-первых, можно ввести для круга совершенно новую структуру, повторив в ней (может быть, под другими именами) те же поля X, Y и Visible и добавив новое поле Radius. Во-вторых, можно сконструировать структуру для круга, используя в ней поле с типом, ранее определенным для точки (сделать «структуру в структуре»). Оба подхода вполне приемлемы, однако ООП предлагает иной подход, который по ряду причин является гораздо более предпочтительным.
Объектно-ориентированный стиль позволяет определить новый объект как потомок другого ранее определенного типа. Это означает, что новый тип автоматически получает все поля и методы ранее введенного типа, который в этом случае называется предком или родительским типом. В этом случае в определении типа-потомка должно быть указано (в круглых скобках после служебного слова object) имя родительского типа:
type
TCircle = object ( TPoint )
Radius : integer
end;
Задание родительского типа означает, что в объектовом типе TCircle неявно присутствуют все поля типа TPoint; аналогично, для переменной этого нового типа доступны все методы из TPoint:
var
OneCircle : TCircle ;
begin
OneCircle.Create(100, 200) ;
OneCircle.Radius := 30 ;
Описанное свойство объектных типов называется наследованием и широко используется в ООП. Один тип может являться предком для произвольного числа типов-потомков, в то время как любой объектовый тип может наследовать поля и методы только одного типа-родителя, который указывается в круглых скобках.
Тип-потомок может, в свою очередь, выступать как предок по отношению к другому объектному типу (типам). Так, можно определить фигуру »кольцо», состоящую из двух концентрических кругов:
type
TRing = object ( TCircle )
Radius2 : integer ;
end ;
Тип TRing наследует поле Radius из своего непосредственного родителя TCircle, а также поля и методы из типа TPoint, который также считается (косвенным) предком для TRing.
Длина такой цепочки наследования в языке никак не ограничивается.
Вернемся к примеру с объектным типом TCircle. По правилу наследования тип TCircle имеет в своем составе методы объекта-предка TPoint. Однако, легко видеть, что если методы GetX, GetY, возвращающие текущие значения координат, вполне применимы в типе-потомке, то, например, методы SwitchOn, SwitchOff не подходят для рисования круга. Поэтому, наряду с основным полем Radius, полное описание типа TCircle должно содержать также собственные методы для рисования и удаления круга и его передвижения по экрану. Самым простым решением было бы ввести в новый тип такие методы, дав им некоторые новые имена. Но объектно-ориентированный подход позволяет определить новые методы со старыми именами, переопределивтем самым методы типа-родителя:
Type
TCircle = object ( TPoint )
Radius : integer ;
procedure Create ( a , b , R : integer ) ;
procedure SwitchOn ;
procedure SwitchOff ;
procedure Move ( dx , dy : integer ) ;
function GetR : integer ;
end ;
procedure TCircle.Create ( a , b , R : integer ) ;
begin
TPoint.Create ( a , b ) ;
Radius := R
end ;
procedure TCircle.SwitchOn ;
begin
Visible := true ;
Graph.Circle ( X , Y , Radius )
end ;
function TCircle.GetR ; integer ;
begin
GetR := Radius
end ;
Такое определение объектного типа TCircle содержит следующие элементы:
- поля X, Y, Visible, унаследованные от родительского типа TPoint;
- собственное поле Radius;
- метод TCircle.Create, который инициализирует поля TCircle. Этот метод переопределяет внутри типа TCircle унаследованный от TPoint метод TPoint.Create. Для инициализации полей X, Y, Visible используется вызов метода TPoint.Create, который доступен (унаследован) в типе TCircle;
- методы TCircle.SwitchOn, TCircle.SwitchOff и TCircle.Move (два последних метода из экономии места не показаны), которые, соответственно, рисуют и удаляют круг и передвигают его. Эти методы полностью переопределяют (замещают) одноименные методы из TPoint, что имеет очевидный смысл: для рисования круга требуется иной алгоритм, нежели для рисования точки (в примере для этих целей используются стандартные процедуры из модуля Graph);
- новый (собственный) метод GetR для доступа к текущему значению радиуса круга;
- два унаследованных (и не переопределенных) метода GetX, GetY для получения текущих координат центра круга.
Сразу отметим, что переопределять можно только методы. Поля, указанные в родительском типе, безусловно наследуются типом-потомком и не могут быть в нем переопределены (то есть имена полей потомка не должны совпадать с именами полей типа-предка). Кроме того, новый метод в типе-потомке может иметь совершенно другие параметры, нежели одноименный метод из типа-предка.
Рассмотрим теперь поведение объекта типа TCircle, то есть вызовы его методов. Пусть имеется следующий фрагмент программы:
var
OneCircle : TCircle ;
begin
with OneCircle do
begin
Create ( 100 , 200 , 30 ) ;
SwitchOn ;
Writeln ( GetX , GetY )
end ;
end ;
Методы Create и SwitchOn определены в объекте OneCircle, поэтому их вызовы однозначно обрабатываются компилятором. Что касается методов GetX, GetY, то они не определены непосредственно в типе Circle, к которому принадлежит объект OneCircle. В таком случае компилятор, не обнаружив вызываемого метода, просматривает определение родительского типа; если данный метод и здесь не определен, то аналогично просматривается родитель родителя и т.д. Если очередной объектовый тип не имеет предка, а метод не обнаружен, то компилятор фиксирует ошибку.
Механизм наследования, являясь достаточно простым для понимания и использования, представляет широкие возможности при разработке программ. Имея несколько «базовых» объектовых типов (например, в интерфейсном разделе модуля), можно на их основе конструировать новые объекты, добавляя в них новые поля и расширяя и/или переопределяя соответствующие методы.
В заключение дадим правило совместимости типов по присваиванию, действующее для случая объектовых типов. Это правило формулируется достаточно просто: совместимыми по присваиванию являются эквивалентные типы, объектные типы, состоящие в отношении наследования, причем присваивание может происходить в направлении от типа-потомка к родительскому типу, но не наоборот. Например, возможно присваивание
OnePoint := OneCircle ;
В подобных случаях копируются (присваиваются) только те поля, которые являются общими для обоих типов. Такое же правило действует и для указателей на объектные типы.