Кнопка под другим именем: когда статическое связывание ошибочно

Кнопка под другим именем: когда статическое связывание ошибочно

К этому моменту должен стать понятным главный вывод из изложенных в этой лекции принципов наследования:

Принцип динамического связывания

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

Рассмотрим вызов x.r. Если x объявлена типа A, но в процессе вычисления была присоединена к объекту типа B, а в классе B компонент r переопределен, то использование в этом вызове исходной версии r из класса A - это не вопрос выбора, это просто ошибка!

Безусловно, имелись причины для переопределения r. Одной из них могла быть оптимизация, как в случае с компонентом perimeter в классе RECTANGLE, но могло также оказаться, что исходная версия r просто некорректно работает для объектов из B. Рассмотрим, например, эскизно описанный класс BUTTON (КНОПКА), являющийся наследником класса WINDOW (ОКНО) в некоторой оконной системе (кнопки являются специальным видом окон). В этом классе переопределена процедура display, так как изображение кнопки немного отличается от изображения обычного окна (например, нужно показать ее рамку). В этом случае, если w имеет объявленный тип WINDOW, но динамически связана, благодаря полиморфизму, с объектом типа BUTTON, то вызов w.display должен исполняться для "кнопочной" версии! Использование display из класса WINDOW приведет к искажению изображения на экране.

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

Это рассуждение можно подкрепить некоторым математическим анализом. Напомним условие корректности процедуры из лекции 11 об утверждениях:

{prer (xr) and INV} Bodyr {postr (xr) and INV}.

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

[A-CORRECT]

{INVA} rA {INVA}

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

[B-CORRECT]

{INVB} rB {INVB}

Напомним, что инварианты накапливаются при движении вниз по структуре наследования, так что INVB влечет INVA, но, как правило, не наоборот.

Рис. 14.14.  Версия родителя может не удовлетворять новому инварианту

Напомним, например, как RECTANGLE добавляет собственные условия к инварианту класса POLYGON. Другой пример, рассмотренный при изучении инвариантов в лекции 11, это класс ACCOUNT1 с компонентами withdrawals_list и deposits_list; его собственный потомок ACCOUNT2 добавляет к нему, возможно, по соображениям эффективности, новый атрибут balance для постоянного запоминания текущего баланса счета. К инварианту добавляется новое предложение:

consistent_balance: deposits_listltotal - withdrawals_listltotal = current_balance

Из-за этого, возможно, придется переопределить некоторые из процедур класса ACCOUNT1; например, процедура deposit, которая использовалась просто для добавления элемента в список deposits_list, сейчас должна будет модифицировать также balance. Иначе класс просто станет ошибочным. Это аналогично тому, что версия процедуры display из класса WINDOW не является корректной для экземпляра класса BUTTON.

Предположим теперь, что к объекту типа B, достижимому через сущность типа A, применяется статическое связывание. При этом из-за того, что соответствующая версия процедуры rA , как правило, не будет поддерживать необходимый инвариант (как, например, depositACCOUNT1 для объектов типа ACCOUNT2 или displayWINDOW для объектов типа BUTTON), будет получаться неверный объект (например, объект класса ACCOUNT2 с неправильным полем balance или объект класса BUTTON, неправильно показанный на экране).

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

Суммируем: статическое связывание является либо оптимизацией, либо ошибкой. Если его семантика совпадает с семантикой динамического связывания (как в случаях (1) и (2)), то оно является оптимизацией, которую может выполнить компилятор. Если у него другая семантика, то это ошибка.