Заметим, что поскольку каждый класс является собственным потомком, то каждый тип согласован сам с собой.
При таком обобщении понятия потомка получаем второе важное правило типизации:
Правило согласования типов
Присоединение к источнику y цели x (т. е. присваивание x:=y или использование y в качестве фактического параметра в вызове процедуры с соответствующим формальным параметром x) допустимо только тогда, когда тип y согласован с типом x.
Правило согласования типов выражает тот факт, что специальное можно присваивать общему, но не наоборот. Поэтому присваивание p := r допустимо, а r := p нет.
Это правило можно проиллюстрировать следующим образом. Предположим, что я настолько ненормален, что послал в компанию Любимцы-По-Почте заказ на "Animal" ("Животное"). В этом случае, что бы я ни получил: собаку, божью коровку или дельфина-касатку, у меня не будет права пожаловаться. (Предполагается, что DOG и все прочие являются потомками класса ANIMAL). Но если я заказал собаку, а почтальон принес мне утром коробку с надписью ANIMAL, или, например, MAMMAL (млекопитающее), то я имею право вернуть ее отправителю, даже если из нее доносится недвусмысленный лай и тявканье. Поскольку мой заказ не был исполнен в соответствии со спецификацией, я ничего не должен фирме Любимцы-По-Почте. |
Экземпляры
С введением полиморфизма нам требуется уточнить терминологию, связанную с экземплярами. Содержательно, экземпляры класса - это объекты времени выполнения, построенные в соответствии с определением класса. Но сейчас в этом качестве нужно также рассматривать объекты, построенные для собственных потомков класса. Вот более точное определение:
Определение: прямой экземпляр, экземпляр
Прямой экземпляр класса C - это объект, созданный в соответствии с точным определением C с помощью команды создания create x ..., в которой цель x имеет тип C (или, рекурсивно, путем клонирования прямого экземпляра C).
Экземпляр C - это прямой экземпляр потомка C.
Из последней части этого определения следует, что прямой экземпляр класса C является также экземпляром C, так как класс входит во множество своих потомков.
Таким образом, выполнение фрагмента:
p1, p2: POLYGON; r: RECTANGLE
...
create p1 ...; create r ...; p2 := r
создаст два экземпляра класса POLYGON, но лишь один прямой экземпляр (тот, который присоединен к p1). Другой объект, на который указывают p2 и r, является прямым экземпляром класса RECTANGLE, а следовательно, экземпляром обоих классов POLYGON и RECTANGLE.
Хотя понятия прямого экземпляра и экземпляра определены выше для классов, они естественно распространяются на любые типы (с базовым классом и возможными родовыми параметрами).
Полиморфизм означает, что элемент некоторого типа может присоединяться не только к прямым экземплярам этого типа, но и к другим его экземплярам. Можно считать, что роль правила согласования типов состоит в обеспечении следующего свойства:
Статико-динамическая согласованность типов
Сущность типа T может во время исполнения прикрепляться только к экземплярам класса T.
Статический тип, динамический тип
Название последнего свойства предполагает различение "статического типа" и "динамического типа". Тип, который используется при объявлении некоторого элемента, является статическим типом соответствующей ссылки. Если во время выполнения эта ссылка присоединяется к объекту некоторого типа, то этот тип становится динамическим типом этой ссылки.
Таким образом, при объявлении p: POLYGON статический тип ссылки, обозначенной p, есть POLYGON, после выполнения create p динамическим типом этой ссылки также является POLYGON, а после присваивания p := r, где r имеет тип RECTANGLE и не пусто, динамическим типом становится RECTANGLE.
Правило согласования типов утверждает, что динамический тип всегда должен соответствовать статическому типу.
Чтобы избежать путаницы напомним, что мы имеем дело с тремя уровнями: сущность - это некоторый идентификатор в тексте класса, во время выполнения ее значение является ссылкой (за исключением развернутого случая), ссылка может быть присоединена к объекту.
У объекта имеется только динамический тип, который он получил в момент создания. Этот тип во время жизни объекта не изменяется.
В каждый момент во время выполнения у ссылки имеется динамический тип, тип того объекта, к которому она сейчас присоединена (или специальный тип NONE, если эта ссылка пуста). Динамический тип может изменяться в результате операций переприсоединения.
Только у сущности имеются и статический, и динамический типы. Ее статический тип - это тип, с которым она была объявлена: если объявление имеет вид x: T, то этим типом будет T. Ее динамический тип в каждый момент выполнения - это тип значения этой ссылки, т.е. того объекта, к которому она присоединена.
В развернутом случае нет ссылки, значением x является объект типа T, и T является и статическим типом и единственно возможным динамическим типом для x. |
Обоснованы ли ограничения?
Приведенные выше правила типизации могут иногда показаться слишком строгими. Например, второй оператор в обоих случаях статически отвергается:
1
p:= r; r := p
2
p := r; x := p.diagonal
В (1) запрещается присваивать многоугольник сущности-прямоугольнику, хотя во время выполнения так получилось, что этот многоугольник является прямоугольником (аналогично тому, как можно отказаться принять собаку из-за того, что на клетке написано "животное"). В (2) компонент diagonal оказался не применим к p несмотря на то, что во время выполнения он, фактически, присутствует.
Но более аккуратный анализ показывает, что наши правила вполне обоснованы. Если ссылка присоединяется к объекту, то лучше избежать будущих проблем, убедившись в том, что их типы согласованы. А если хочется применить некоторую операцию прямоугольника, то почему бы сразу не объявить цель прямоугольником?
На практике, случаи вида (1) и (2) маловероятны. Присваивания типа p:= r обычно встречаются внутри некоторых управляющих структур, которые зависят от условий, определяемых во время выполнения, например, от ввода данных пользователем. Более реалистичная полиморфная схема может выглядеть так: