Объектно-ориентированное программирование и программная инженерия

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

Показывать лекцию целиком

Мир (готовы ли вы уже в самом начале лекции погрузиться в водовороты жизни?) полон беспорядка. Возможно, Природа ненавидит беспорядок, а может быть — и нет, я в этом не уверен. Ваша точка зрения во многом зависит от того, кого вы читали — Платона, Аристотеля, Канта. Наука определенно борется с беспорядком. Здравые рассуждения о мире требуют порядка.

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

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

Чтобы стать науками, ботанике и зоологии нужен был Линней, который в 18-м столетии создал эффективную классификацию живых существ. Биологическая таксономия говорит нам, что дельфины принадлежат виду, называемому по латыни Delphinius delphis, они включены в род Delphinius, который сам является частью — пропуская некоторые уровни классификации — отряда китовых из класса млекопитающих, принадлежащего, вне всякого сомнения, царству животных Основная классификация Линнея обычно содержит 7 уровней: царство -> тип -> класс -> отряд -> семейство -> род -> вид. Подробная классификация может содержать более 30 уровней, вводя имена промежуточных уровней и создавая уровни с использованием приставок -подтип, надкласс, суперкласс, подотряд и так далее. Для именования объектов используются два последних уровня род и вид. Мы, люди, согласно этой классификации относимся к Homo sapiens (род - человек, вид - разумный). Как и дельфины, мы входим в класс млекопитающих из царства животных.

Объекты в этом случае естественные, а классификация искусственная.

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

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

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

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

Чтобы справиться с беспорядком, подобно всем наукам, мы можем использовать таксономию. Мы можем объединить наши объекты в категории и рассматривать иерархические отношения между этими категориями. Как дельфин, будучи млекопитающим, является позвоночным животным, как поле в математике является кольцом, так и такси, моделируемое в нашей программе, является транспортным средством и как таковое является движущимся городским объектом. Пешеход также является движущимся городским объектом, но не является транспортным средством. Наследование позволит нам делать выводы о таких отношениях, как "является" ("is—a") и использовать таксономии для структурирования нашего ПО.

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

Такси и транспортные средства

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

Наследуемые компоненты

Класс TAXI из TRAFFIC обеспечивает — вы можете это проверить — такой компонент, как

take (from_location,to_location: LOCATION)
    — Доставить пассажира из from_location в to_location

Другим компонентом класса является office, представляющий диспетчерский офис службы такси.

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

  • такси имеет пассажиров (в противном случае комментарий для компонента take не имел бы смысла: кого следует доставить из одной точки в другую?). Класс должен иметь команду для посадки пассажиров и запрос, позволяющий выяснить текущее число пассажиров;
  • в любой момент такси имеет текущую позицию.
  • Где же находятся соответствующие свойства? Ответ можно найти, взглянув в начало объявления класса:

    note
    …
     class
      TAXI
    inherit
      VEHICLE
    feature
     ... Оставшаяся часть класса...
    

    Класс TAXI наследует от VEHICLE ; и в самом деле, если посмотреть на класс VEHICLE, то можно найти команды load, для посадки пассажиров в транспортное средство, так же как и unload — для высадки, и запрос count, дающий текущее число пассажиров. Теперь, обратившись к началу объявления класса VEHICLE, вы увидите:

    note
    ...
    deferred class
      VEHICLE
    inherit
      MOVING
    feature
    …Оставшаяся часть класса...
    

    Класс VEHICLE наследует от класса MOVING, который описывает движущиеся объекты и имеет запрос position.

    Классы deferred class. Мы вскоре познакомимся детально с концепцией отложенного класса, указывающего на то, что не все его компоненты полностью заданы; реализация некоторых из них оставлена его потомкам — классам, наследующим от него.

    Глядя, как эти три класса описывают типы объектов периода выполнения — такси, транспортные средства, движущиеся объекты, — мы понимаем, о чем говорит нам наследование. Оно устанавливает, что в системе Traffic любое такси может рассматриваться как транспортное средство, которое, в свою очередь, является движущимся объектом. В частности, все свойства класса MOVING применимы к целям типа VEHICLE и TAXI, и все свойства класса VEHICLE применимы к целям типа TAXI.

    Термины наследования

    Как обычно, помогает точная терминология.

    Определения: наследник, родитель, (правильный) потомок и предок

    Если B наследует от A ( В Eiffel A перечислено в предложении inherit класса B), то B наследник A, а A - родитель B. Потомками класса является сам класс и (рекурсивно) потомки его наследников. Сам класс не включается в число правильных потомков. Предок и правильный предок являются обращенными понятиями по отношению к потомкам.

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

    В литературе иногда встречается термин "подкласс", означающий иногда наследника, иногда правильного потомка. Аналогично встречается и термин "суперкласс".

    На рисунке ниже, иллюстрирующем наш пример, все классы являются потомками класса MOVING. Все классы — его правильные потомки, за исключением самого класса. Правильными предками класса TAXI являются классы VEHICLE и MOVING.

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

    (рис 1.1) Иерархия наследования

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

    От вас не требуется вручную рисовать диаграмму, отображающую отношения между классами. Если классы скомпилированы, то инструментарий Diagram Tool EiffelStudio создаст диаграмму. Достаточно просто щелкнуть вкладку Diagram, и появится требуемая диаграмма:

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

    Компоненты, приходящие от высших авторитетов

    Мы можем теперь оценить новинку, вводимую наследованием. Понятие "компоненты класса" больше не означает только компоненты, заданные в классе, но и компоненты, наследуемые от родителя. Так, объявив объекты m: MOVING, v: VEHICLE, t: TAXI, мы можем помимо прочего писать:

    v.load (...)
    t.take (...)
    v.count   — Выражение
    

    Возможны и другие вызовы, такие как:

    t.count
    t.load
    t.position
    v.position
    

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

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

    Компонент класса - это одно из двух:

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

    Отныне следует понимать, что класс А больше, чем то, что видимо в тексте класса. Компоненты класса означают не только компоненты, объявленные в тексте класса, но и те, что приходят от родителей, если таковые существуют.

    Плоский облик

    Как тогда получить полную картину? Плоским обликом класса называется искусственно сконструированная версия, которая включает все компоненты, непосредственные и наследуемые. Это не то, что вы пишете, создавая класс, но лишь облик, подобный контрактному облику, который, как мы видели, дает нам свободную от реализации версию класса. EiffelStudio создает этот взгляд на класс, этот облик, для чего достаточно выбрать класс и щелкнуть по кнопке "Flat view" :

    (рис 1.3)

    Результат похож на обычный текст класса (с некоторой новой нотацией, которая позже прояснится). Отметьте появление нового вида комментариев, здесь для компонента LINKED_LIST :

    (рис 1.4) Плоский облик

    Выделенных комментариев нет в исходном тексте, но они добавлены EiffelStudio, когда он создает плоский облик. Они указывают, что компонент наследуется, приходя от правильного предка CHAIN, и что его предусловие было определено в другом предке — LINEAR. Мы вскоре увидим, как контракты преобразуются в связи с наследованием.

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

    Полиморфизм

    Аккумулирование компонентов не является единственной целью наследования. Мы уже демонстрировали эту возможность, когда писали класс 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 для случая полиморфной цели. Ответ ведет нас к еще одной фундаментальной ОО-концепции.

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

    В вызове, таком как 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), не будет обнаружено на этапе трансляции, и ошибка проявится только во время выполнения, приводя к аварийному завершению программы. Цель такого подхода в том, чтобы избежать излишних проверок в период компиляции и обеспечить большую гибкость.

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

    Вернуться к учебному плану