Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Методичка ООП.doc
Скачиваний:
22
Добавлен:
08.11.2018
Размер:
1.4 Mб
Скачать
  1. Приведение объектных типов, операторы as и is

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

Вернемся к рассмотренному ранее примеру: пусть имеется иерархия графических объектов, например прямоугольник, рисунок и рамка текста. Причем рисунок и рамка текста являются потомком класса «прямоугольник». Они используют наследуемые от него методы, такие как: определить/установить координаты занимаемой области, проверить принадлежит ли точка занимаемой области и так далее. Эти методы для всех классов совпадают и по назначению и по реализации, поэтому объявляются как статические в классе-предке. Есть ряд методов, которые являются общими по назначению, но отличаются по своей реализации. Например, объекты всех этих классов должны уметь отображать себя на экране. Метод отображения будет иметь одинаковый интерфейс, но разную реализацию для разных классов. Такие методы объявляют как виртуальные или динамические в предке и перекрывают в потомках. Но некоторые поля методы и свойства специфичны для каждого из потомков не только по реализации, но и по назначению. Например, для рамки текста должны быть определены функции по обработке текста, а для рисунка – действия по работе с изображением (рис. 12).

Если работать с объектом типа класс-потомок через указатель на класс-предок, например, когда разнотипные объекты собраны в список, то вызвать такие методы непосредственно нельзя, у класса-предка их просто нет. Для решения этой проблемы используется приведение объектных типов.

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

Определить приведение объектных типов можно как операцию, изменяющую тип указателя на объект.

Для объектных типов, как и для любых других, допустимо явное приведение типов:

TAnotherType(ASomeObject).Method1

В этом примере при вызове метода Method1 указатель на соответствующий объект ASomeObject будет иметь тип TAnotherType. Проверки совместимости типов при этом не проводится. Поэтому использовать такой способ не рекомендуется.

Для приведения объектных типов в языке Object Pascal имеется специальный оператор as. Выражение

object as class

возвращает указатель на объект object как указатель на объект типа class. С его помощью можно рассматривать указатель на экземпляр объекта как принадлежащий к другому совместимому типу:

(ASomeObject as TAnotherType).Method1

В этом примере при вызове метода Method1 указатель на соответствующий объект ASomeObject, как и в предыдущем примере, будет иметь тип TAnotherType.

После приведения сам объект остается неизменным, изменяется только тип указателя на него – то есть становятся доступными элементы, определенные в том классе, к которому выполняется приведение.

Рассмотрим пример приведения объектных типов с использованием оператора as:

Interface Type TSomeOb1 = class Procedure Method1; end;

TSomeOb2 = class(TSomeOb1) Procedure Method2; end;

TSomeOb3 = class(TSomeOb2) Procedure Method3; end;

Implementation Procedure Test; var Ob: TSomeOb1;

Begin Ob := TSomeOb3.Create; Ob.Method1; //допустимо //Ob.Method2; //ошибка компиляции //Ob.Method3; //ошибка компиляции (Ob as TSomeOb2).Method1; //допустимо (Ob as TSomeOb2).Method2; //допустимо //(Ob as TSomeOb2).Method3; //ошибка компиляции (Ob as TSomeOb3).Method1; //допустимо (Ob as TSomeOb3).Method2; //допустимо (Ob as TSomeOb3).Method3; //допустимо Ob.Free; end;

В этом примере указатель имеет тип TSomeOb1. В классе TSomeOb1 определен единственный метод Method1, поэтому через этот указатель без приведения типов может быть вызван только этот метод. Попытка вызвать методы Method2 и Method3 приведет к ошибке компиляции, несмотря на то, что реальный класс объекта TSomeOb3, в котором определены все три метода. При приведении указателя к классу TSomeOb2 допустимым также стал вызов метода Method2, определенного в классе TSomeOb2, а при приведении к классу TSomeOb3 можно вызывать все три метода.

От стандартного способа приведения типов использование оператора as отличается наличием проверки на совместимость типов как во время компиляции, так и во время выполнения. Правило приведения типов - приводить объект можно либо к его реальному классу, либо к классу, который является предком его реального класса. Проверка выполняется в два этапа:

1. При компиляции производится формальная проверка совместимости типов – если указатель на объект формально не может указывать на объект такого типа, то компилятор выдаст ошибку в этом операторе. Ошибки не будет, если мы пытаемся привести объект к типу класс, который является производным для типа указателя, так как вполне возможно, что реальный объект будет иметь именно этот тип или тип одного из его потомков.

2. На этапе выполнения программы. Формальная проверка на этапе компиляции не является гарантией правильности приведения, поскольку возможно, что мы пытаемся привести объект к классу, который не совпадает с его реальным классом и не является предком его реального класса, хотя и его реальный класс и класс, к которому приводим, являются потомками одного класса. В этом случае проверка обнаруживает попытку приведения к несовместимому типу, и это приводит к ошибке периода выполнения программы, точенее к возникновению исключительной ситуации (ИС). Исключительные ситуации рассматриваются в соответствующем разделе.

Рассмотрим иерархию наследования классов, представленную на рис. 13 и попробуем применить к ней изложенные выше правила. Тип указателя TObject1, реальный тип объекта во время компиляции еще не известен.

На этапе компиляции проверяется, может ли в принципе данный указатель указывать на объект того типа, к которому приводим. А указывать он может либо на объект класса указателя TObject1, либо на объект одного из его потомков: TObject11, TObject12, TObject121, TObject1211. Попытка приведения к типу TObject2, как впрочем, и к TObject, которые не являются его потомками, приведет к ошибке компиляции. При выполнении программы реально был создан объект класса-потомка TObject121. С этим объектом можно работать как с объектом этого типа, или типа одного из его предков: TObject121, TObject12, TObject1. Таким образом, попытка привести объект к типу TObject1211 вызовет ошибку периода выполнения.

Пример приведения объектных типов с проверкой совместимости и без нее:

Interface Type TDat = class(TObject) Procedure X; ... end;

TDat1 = class(TDat) Procedure X; ... end;

TDat2 = class(TDat) Procedure X; ... end;

Implementation Procedure Test; var

D: TDat; D1: TDat1; Begin D := TDat1.Create; D1 := TDat1.Create; TDat1(D).X; //будет вызван метод класса TDat1, что правильно TDat2(D).X; //будет вызван метод класса TDat2, что неправильно TDat2(D1).X; //будет вызван метод класса TDat2, что неправильно (D as TDat1).X; //будет вызван метод класса TDat1, что правильно (D as TDat2).X; //вызовет исключительную ситуацию во время выполнения (D1 as TDat2).X; //вызовет ошибку периода компиляции D.Destroy; D1.Destroy; End;

В данном примере имеется иерархия классов TDat – класс-предок и два его потомка: TDat1 и TDat2. Объявлены два указателя: первый имеет тип TDat, второй тип потомка TDat – TDat1. Реально созданы два объекта типа TDat1. Рассмотрим сначала стандартное приведение типов. Операторы TDat2(D).X и TDat2(D1).X приведет к вызову метода класса TDat2, хотя реальный тип объекта TDat1. Следовательно, в обоих случаях будет вызван метод, не соответствующий реальному типу объекта, что может привести к совершенно непредсказуемым ошибкам. В случае приведения типов с использованием оператора as: (D as TDat2).X не вызовет ошибки периода компиляции, так как тип указателя TDat формально совместим с типом TDat2, к которому приводим. Но во время выполнения программы возникнет исключительная ситуация, так как реально созданный объект имеет тип TDat1, который не совместим с типом TDat2. Возникшая исключительная ситуация далее может быть корректно обработана. Оператор (D1 as TDat2).X вызовет ошибку периода компиляции, так как типы формально не совместимы: тип TDat2, к которому приводим, не является потомком TDat1 – типа указателя D1. То есть эта ошибка будет найдена еще до выполнения программы.

Поскольку существуют ограничения на приведение объектных типов, иногда возникает необходимость проверять возможность приведения еще до его выполнения. Для этого может использоваться оператор is, проверяющий совместимость по присваиванию объекта с классом и возвращающий true если типы совместимы, т.е. объект является экземпляром данного класса или одного из его потомков, и false в ином случае. Если оператор is при проверке совместимости объекта с классом возвращает true, то этот объект можно приводить к этому классу.

Рассмотрим пример совместного использования операторов is и as. Для этого вернемся к иерархии классов – геометрических фигур:

Type TRectangle = class //прямоугольник ... procedure Draw; virtual; end;

TPicture = class(TRectangle) //картинка ... procedure Draw; override; end;

TText = class(TRectangle) //рамка текста ... procedure Draw; override;

end;

У всех этих классов есть метод рисования фигуры на экране Draw. Он одинаков по назначению и интерфейсу, но различается по реализации. Поэтому он объявлен как виртуальный в классе-предке и перекрывается во всех потомках. Предположим, что для вывода текста в рамку текста решили использовать специальный метод:

TText = class(TRectangle) ... //метод Draw не перекрывается, поскольку рисование рамки //не отличается от рисования обычного прямоугольника procedure OutText; end;

Если работать с объектами этих классов как с массивом или передавать их в качестве параметров подпрограммы, доступ к виртуальному методу Draw любого объекта не представляет труда. Но для доступа к методу TText.OutText необходимо явно приводить типы. Оператор is используется, чтобы проанализировать, что данный объект действительно является рамкой текста. В этом случае его тип приводится к типу класса рамки текста и вызывается его метод OutText. Например:

Procedure Test; var Figure: array[1..100] of TRectangle; i,n: integer; begin n := 100; Figure[1] := TText.Create; Figure[2] := TRectangle.Create; Figure[3] := TText.Create; Figure[4] := TPicture.Create; ... Figure[n] := TText.Create; for i:=1 to n do begin Figure[i].Draw; //будет вызван метод Draw соответствующий //реальному типу объекта if Figure[i] is TText then //если это рамка текста Figure[i] as TText).OutText; //вывод текста end; ... for i:=1 to n do Figure[i].Destroy; end;

Если бы в данном примере вместо строки

(Figure[i] as TText).OutText

было написано

Figure[i].OutText

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

Приведение объектных типов приводит к изменениям в вызове только статически перекрытых методов. Виртуальные и динамические методы по-прежнему вызываются в соответствии с реальным типом объекта:

Interface Type TSomeOb = class public Procedure X; virtual; Procedure Y; ... end;

TSomeOb1 = class(TSomeOb) public Procedure X; override; Procedure Y; ... end;

TSomeOb2 = class(TSomeOb1) public Procedure X; override; Procedure Y; ... end;

Implementation Procedure Test; var Ob: TSomeOb; Begin Ob := TSomeOb2.Create; (Ob as TSomeOb1).X; //будет вызван метод класса TSomeOb2 в соответствии с //реальным типом объекта, так как метод X виртуальный (Ob as TSomeOb1).Y; //будет вызван метод класса TSomeOb1 в соответствии с // типом класса, к которому приводим, так как метод Y //статический Ob.Destroy; Ob := TSomeOb1.Create; (Ob as TSomeOb2).X; //вызовет исключительную ситуацию (Ob as TSomeOb2).Y; //вызовет исключительную ситуацию Ob.Destroy; End;

В этом примере в тестовой процедуре объявлена переменная, типа класса-предка TSomeOb. Сначала создается объект типа TSomeOb2, который является потомком класса TSomeOb1, являющегося, в свою очередь, потомком класса TSomeOb. Приведение указателя к классу TSomeOb1 влияет только на вызов статически перекрытого метода Y. Для виртуального метода X как с приведение типов, так и без него будет вызываться реализация, соответствующая реальному типу объекта TSomeOb2. Во втором случае реально создан объект класса TSomeOb1, а приводится указатель к типу TSomeOb2, который является его потомком. Это ошибочное приведение типов. Оно приведет к возникновению исключительной ситуации как для статического метода Y, так и для виртуального метода X.

    1. Приведение объектных типов в C++

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

Для указателей на динамические объекты, как и для любых других указателей, в языке С++ нет синтаксических ограничений на явное приведение типов:

class С1 { ... };

С1 * с = new C1(); CSomeClass x = (CSomeClass *) c; x->SomeMethod();//вызовется метод класса CSomeClass ((CSomeClass *) c)->SomeMethod();//вызовется метод CSomeClass

При этом изменится только тип указателя, тип объекта останется неизменным.

Приведение указателя к типу указатель на класс-предок может быть неявным.

Несмотря на то, что синтаксис допускает любое преобразование типов указателей, понятно, что программа будет работать правильно только в том случае, если указатель будет иметь тип, соответствующий реальному типу объекта или одному из его предков. Обеспечение правильности приведения типов указателей остается задачей разработчика.

Например:

class С1 { ... public int SomeFunction1(int); };

class С2: public С1 { ... public int SomeFunction2(int); };

class С3 { ... public int SomeFunction3(int); };

... C1 * c1 = new C2(); //создаем объект класса С2

C2 * c2 = (C2 *) c1; //явное преобразование к реальному //типу объекта – синтаксически допустимо и правильно с2->SomeFunction1(); с2->SomeFunction2();

C3 * c3 = (C3 *) c1; //преобразование к указателю на объект //произвольного класса – синтаксически допустимо, но приведет //к неправильной работе программы с3->SomeFunction3(); //будет пытаться работать с полями, //определенными в C3, но на самом деле поля соответствуют C2 C1 * c = c2; //преобразование к указателю на передка – может //быть неявным

Возможность проверки реального типа динамического объекта во время выполнения программы не входит в стандарт языка С++, но предоставляется некоторыми компиляторами, например Borland C++ Builder. Здесь эта возможность рассматриваться не будет.

Для статических объектов преобразования типов происходят при выполнении операции присваивания. По умолчанию определено преобразование объекта типа класс-потомок к типу класса-предка:

class С1 { ... };

class С2: public С1 { ... };

С2 с2; С1 с1; с1 = с2; //присваивание допустимо //с2 = с1; //присваивание недопустимо

При этом происходит побитовое копирование полей объекта. Если нужны какие-либо иные действия, например, в случае полей-указателей, операцию копирования для класса-предка можно перегрузить (про перегрузку операций говорится в разделе «Перегрузка операций в С++»):

class С1 { ... public: void operator = (const С1 & с) {...}; ...

};

Следует различать копирование и инициализацию. Инициализация происходит при создании статического объекта, копирование – при выполнении оператора присваивания. В случае инициализации другим объектом того же или производного класса вызовется конструктор копирования, определенный разработчиком или созданный по умолчанию. В случае копирования выполняются действия оператора присваивания:

class С1 { ... };

class С2 : public С1 { ... };

С2 с2; С1 с1 = с2; //инициализация – будет вызван конструктор //копирования класса С1

с1 = с2; //копирование – выполнится оператор присваивания

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

Преобразование типа к объектному типу определяется конструктором, имеющим единственный аргумент того типа, преобразование из которого производится:

class С1 { protected: int SomeData; ... public: С1(int); //преобразование от типа int к типу С1 ... };

С1::C1(int x) { SomeData = x; }

... C1 c; c = 10; //выполнится преобразование С1::C1(10)

При выполнении такого присваивания соответствующим конструктором будет создан временный объект и проинициализированы его поля. А далее выполнится оператор присваивания, определенный для объектов соответствующего типа – его реализация по умолчанию, как уже было сказано, просто копирует поля объекта.

Это же преобразование может быть вызвано явно:

C1 c; c = C1(10);

Для того чтобы описать обратное преобразование от объекта к какому-либо типу, у класса объявляется функция-элемент:

operator type();

Например:

class C1 { protected: int SomeData; ... public: operator int(); };

C1::operator int() { return SomeData; }

Это преобразование может использоваться неявно или явно:

С1 с = 3; //инициализация int a = c; //неявно будет вызвано преобразование operator int() int b = int(c); //явное преобразование int b = (int) c; //явное преобразование

Таким образом, присваивание значения объекту класса Х разрешено в случае, если тип T присваиваемой величины есть тип Х, тип-потомок класса Х, или если объявлено преобразование от T к Х. Также можно объявить обратное преобразование.