Опубликован: 04.04.2012 | Доступ: свободный | Студентов: 1989 / 61 | Оценка: 4.60 / 4.40 | Длительность: 13:49:00
Лекция 2:

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

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

Динамическое связывание

В вызове, таком как v.load(.), цель v является полиморфной, так что во время выполнения она может быть присоединена к объекту типа TAXI, или TRAM, или к любому потомку VEHICLE.

Если теперь вы посмотрите на текст этих классов, то увидите, что каждый из них имеет собственную реализацию метода load (ниже мы увидим, как создавать такие объявления без двусмысленностей и конфликтов). Какая же версия должна реально вызываться?

Единственно правильный ответ — мы хотим вызывать правильный компонент. Правильный в том смысле, что он наиболее точно соответствует типу объекта, к которому присоединена сущность v в момент выполнения. Когда этот объект является экземпляром TAXI, мы хотим, чтобы вызывалась версия класса TAXI, когда TRAM — то TRAM -версия, и так далее.

 Знакомое наследование

Рис. 1.7. Знакомое наследование

Любое другое решение было бы некорректным. Глупо было бы применять операцию посадки в трамвай при посадке в такси. Даже применять операцию по умолчанию, из класса VEHICLE, даже если она там предусмотрена, нецелесообразно. Понятно, что автор класса TAXI, который задал реализацию load, специально приспособленную для посадки в такси, был бы крайне удивлен, если бы использовалась для этих целей другая реализация, предназначенная для посадки в другое транспортное средство.

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

Эта политика, являясь краеугольным камнем конструирования ОО ПО, имеет собственное имя.

Определение: динамическое связывание
Динамическое связывание (семантическое правило) требует, чтобы при вызове компонента использовалась та версия компонента, которая наилучшим образом адаптирована к типу целевого объекта.

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

Типизация и наследование

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

Правило прямолинейно: при полиморфном присоединении тип источника должен быть потомком типа цели. Это не вполне подходящая терминология, — ниже мы увидим, как сделать ее корректной, — но она выражает основную идею. Отсюда следует, что наш прежний пример присваивания my_vehicle:= cab_at_corner является корректным, а присваивание my_vehicle:= Paris — недопустимо. Город Париж не является транспортным средством.

Это правило стоит за разнообразием возможностей, обсуждаемых ранее. Если мы допускаем присваивание x:= y, где x типа T, а y типа U, мы должны быть уверенными, что вызов x.f будет иметь смысл во время выполнения, не только если целевой объект типа T, но и в том случае, если он имеет тип U, при условии, что вызов признан корректным во время компиляции. Это условие корректности является обычным и основывается на объявлении x, оно устанавливает, что f должно быть компонентом T. Но нам нужны гарантии, что f также является компонентом U. Это выполняется по определению, если U потомок T.

Следующие термины помогают в понимании этих концепций.

Статический тип, динамический тип

Статическим типом сущности или выражения e является тип, используемый в объявлении в соответствующем тексте класса.

Если e во время конкретного выполнения присоединено к объекту, то тип этого объекта будет динамическим типомe.

После присваивания my_vehicle:= cab_at_corner сущность my_vehicle имеет динамический тип TAXI. Ее статический тип является типом, заданным в объявлении — VEHICLE.

Эти понятия применимы к сущностям и выражениям. Для объектов такого различия типов нет. Для них определен только динамический тип, появляющийся в момент создания объекта, — это тип TAXI, или TRAM, или что-либо еще, но всегда остающийся неизменным конкретный тип объекта.

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

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

  • TAXI (не имеет родовых параметров) согласовано c VEHICLE, так как является его потомком;
  • LINKED_LIST[TAXI] согласовано с LIST[TAXI], так как LINKEDLIST является потомком LIST и фактический родовой параметр один и тот же.

Дадим более точное определение этого свойства.

Определение: согласование

Если класс D является потомком класса C и оба не развернутые, то типы, выводимые из D, согласованы с теми, что выводимы из C, следующим образом:

  • если класс D не являются универсальным, то D (как тип) согласован с C ;
  • если класс D является универсальным, то D[T, U,...] согласован с C[T, U,...] (с теми же самыми родовыми параметрами).

Развернутый тип согласован только с собой.

Полное определение допускает соответствие и в том случае, когда фактические родовые параметры не совпадают, но согласованы друг с другом. Так, LINKED_LIST[TAXI] согласован с LINKED_LIST[VEHICLE] (следовательно, и с LIST[VEHICLE] ), поскольку LINKEDLIST согласован с LIST, а класс TAXI согласован с VEHICLE. Этот случай требует особой внимательности, и нам он не нужен. Как обычно, когда возникает какая-либо неясность, следует обращаться к стандарту языка для полного понимания.

Все это нам понадобилось, чтобы дать корректную версию правила полиморфизма. Точное правило будет ссылаться на "согласование", а не на свойство "является потомком".

Правило полиморфизма типов

Чтобы полиморфное присоединение было правильным, тип источника должен быть согласован с типом цели.

Это подтверждает, что полиморфизм применим только к ссылочным типам, так как по определению развернутый тип согласован только с самим собой.

Правило типизации обеспечивает нам безопасность, гарантируя, что в фундаментальной операции ОО-программирования

x..f (...)

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

Интересно отметить комбинацию свойств, которые характерны для большинства современных ОО языков программирования.

  • Статическая типизация, гарантирующая, что существует по меньшей мере один компонент для f.
  • Динамическое связывание, гарантирующее использование лучшего компонента — того, что непосредственно подходит типу объекта, если доступно более одного варианта реализации f.

Язык Smalltalk выбирает из этой политики комбинацию динамического связывания и динамической проверки типов. Как результат, неверное применение вызова компонента, например, Paris. load(.) (компонент класса VEHICLE применяется к цели класса CITY), не будет обнаружено на этапе трансляции, и ошибка проявится только во время выполнения, приводя к аварийному завершению программы. Цель такого подхода в том, чтобы избежать излишних проверок в период компиляции и обеспечить большую гибкость.

Другая группа ОО-языков в целях улучшения производительности имеет тенденцию применять статическое связывание.

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

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