Опубликован: 04.04.2012 | Уровень: для всех | Доступ: платный
Лекция 2:

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

< Лекция 1 || Лекция 2: 123 || Лекция 3 >

Полиморфизм

Аккумулирование компонентов не является единственной целью наследования. Мы уже демонстрировали эту возможность, когда писали класс PREVIEW как наследника класса TOURISM. Мы хотим получить преимущества от наследования не просто за счет модульности, а за счет того, что оно задает отношения между типами, — дельфины являются млекопитающими, кольца являются группами.

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

 my_vehicle: VEHICLE
 cab_at_corner: TAXI

Структура наследования, рассмотренная выше, делает допустимым присваивание:

my_vehicle := cab_at_corner

Оно отличается от присваивания, которое нам встречалось до сих пор. Ранее выражение источника присваивания и переменная, задающая цель присваивания, были одного типа, но теперь они различны. Специфика в том, что тип источника является потомком типа цели.

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

 Полиморфное присваивание

Рис. 1.5. Полиморфное присваивание

Это обычное ссылочное присваивание. Сами объекты — здесь объекты типов TAXI и VEHICLE — не меняются. Новинка в том, что после присваивания переменная типа VEHICLE теперь может быть присоединена к объекту типа TAXI — к объекту одного из своих потомков.

Определения

Нам нужна подходящая для этой ситуации терминология.

Определения: полиморфизм

Присоединение (присваивание или передача аргумента) является полиморфным, если целевая переменная и выражение источника имеют различные типы.

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

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

Напоминаю, что "сущности" включают переменные (атрибуты, локальные переменные), а также формальные аргументы методов и Result.

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

Как отмечалось в определении, полиморфизм существует не только при присваивании, но и при передаче аргумента в момент вызова метода. Пусть некоторый произвольный класс, например, DAILYSCHEDULE, имеет метод:

register_trip (v: VEHICLE)

Тогда вызов этого метода является вполне корректным:

register_trip (cab_at_corner)

Здесь тип фактического аргумента является потомком типа формального аргумента. Наиболее интересно здесь то, что, когда пишется метод, такой как register_trip, то используется не полное, а частичное знание. Автор знает, что во время выполнения значение аргумента будет присоединено к объекту, представляющему некоторый вид транспортного средства — VEHICLE, но этот объект может быть TAXI, или TRAM, или любым другим транспортным средством, и автор метода не знает, каким именно, и ответ может изменяться от одного выполнения к другому.

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

Полиморфизм - это не трансформация

Несмотря на свое имя — от греческого словосочетания "множественность форм" — полиморфизм не является причиной изменения во время выполнения объектом своей "формы" (изменением типа). Полиморфное присоединение применимо только к ссылочным типам с эффектом, показанным на последнем рисунке, — изменяются ссылки, но сами объекты не меняют тип.

Иногда возникает необходимость трансформации объекта, но это никак не связано с полиморфизмом. Простейшим примером такой ситуации является присваивание целого целевой переменной типа REAL, чье внутреннее представление отличается от представления источника. Подходящим механизмом в таком случае является трансформация, но не полиморфное присоединение.

Общий механизм трансформации, применимый как к ссылочным, так и к развернутым типам, механизм, который стоит за возможностью присваивания целых вещественным переменным, поддерживается в Eiffel. Язык позволяет также определять собственную трансформацию между создаваемыми типами данных. Если проектируется класс DATE с атрибутами day, month, year, то в класс можно включить метод, осуществляющий преобразование из DATE в STRING, что позволит преобразовать дату и представить ее в виде строки текста (например, в виде "13.06.2010" или в любом другом формате, выбранном в методе преобразования).

Мы не будем более останавливаться на механизмах преобразования. Если необходимо его использовать, то можно проанализировать для начала класс REAL32 в EiffelBase (смотри предложение convert ), этого будет достаточно для понимания основных идей трансформации.

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

Полиморфные структуры данных

Особый интерес представляет последний случай в определении полиморфизма — полиморфная структура данных, называемая также полиморфным контейнером. Рассмотрим типичный контейнер — список, предназначенный для хранения транспортных средств:

fleet: LIST [VEHICLE]

Рассмотрим вызов, такой как fleet.extend(...), добавляющий элемент в список. Какой вид аргументов можно использовать в вызове, заменяя "..."? Если посмотреть на объявление extend в LIST[G], то можно видеть:

extend (v: G)
    — Добавить новое вхождение v в конец.

В случае с fleet фактическим родовым параметром, соответствующим G, является тип VEHICLE, так что вызов ожидает аргумента типа VEHICLE, как в вызове: feet.end (myvehicle)

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

fleet.extend ( cab_at_corner)

В общем случае, аргумент может быть любого типа, являющегося потомком VEHICLE, таким как TAXI, TRAM и другие.

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

 Полиморфный список

Рис. 1.6. Полиморфный список

Список содержит смесь объектов различных типов, все из них являются потомками VEHICLE (включая BUS, не появлявшийся ранее).

Возможность построения таких полиморфных структур данных — результат комбинации двух фундаментальных ОО-механизмов, наследования и универсальности. Это дает нам новый уровень гибкости. Рассмотрим, например, запрос last, результатом которого будет последний элемент списка. Он объявлен в классе LIST[G] и возвращает результат типа G. Сущность fleet объявлена как fleet: LIST[ VEHICLE], используя VEHICLE в качестве фактического родового параметра для G. Поэтому выражение:

fleet.last

будет иметь тип VEHICLE. В каждом конкретном случае результирующий объект может быть объектом любого из потомков. Если список находится в состоянии, показанном на последнем рисунке, объектом будет TAXI, но это может быть и другой тип, и вы не знаете, какой именно. Но это и не нужно знать, поскольку к результату можно применять любую компоненту класса VEHICLE. После объявления v: VEHICLE и присвоения v:=fleet. last допустимы вызовы v.load(...) и v.count. Эти вызовы компонентов класса VEHICLE корректны, но, конечно же, нельзя вызывать компоненты классов потомков, например, v.take(...), так как take ожидает не просто VEHICLE аргумент, а TAXI.

Я могу услышать от вас: это несправедливо! Просто взгляните на последний рисунок, ведь последний объект — такси. Почему же я не могу выполнить операцию, вполне допустимую для этого объекта?

Не горячитесь.

Во-первых, жизнь полна несправедливостей, и нужно уметь принимать ее такой, какой она есть.

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

И третье: все будет хорошо — и это настоящий ответ! Существуют способы проверки того, чем является полученный объект, является ли он в самом деле такси в данном конкретном выполнении, и если да, то вызов take можно сделать законным. Но прежде чем узнать, как это делается, придется прочесть еще несколько десятков страниц. Разве я не говорил вам, что жизнь полна несправедливостей?

В данный момент более важно понимание эффекта правильных вызовов — вызовов компонентов VEHICLE, таких как v.load(...) и v.count для случая полиморфной цели. Ответ ведет нас к еще одной фундаментальной ОО-концепции.

< Лекция 1 || Лекция 2: 123 || Лекция 3 >
Надежда Александрова
Надежда Александрова

Уточните пожалуйста, какие документы для этого необходимо предоставить с моей стороны. Курс "Объектно-ориентированное программирование и программная инженения". 

Юрий Симонов
Юрий Симонов
Россия, Москва, Московский Государственный Университет им. М.В. Ломоносова, 2011
Юрий Бедарев
Юрий Бедарев
Россия, Новосибирская область