Все подклассы на рис. 3-5 являются также подтипами вышестоящего класса. В частности, ElectricalData является подтипом TelemetryData. Система типов, развивающаяся параллельно наследованию, обычна для объектно-ориентированных языков с сильной типизацией, включая C++. Для Smalltalk, который едва ли вообще можно считать типизированным, типы не имеют значения.
Поиск метода
Рассмотрим иерархию (рис. 3-6), в которой имеется базовый класс и три подкласса с именами circle, Triangle и Rectangle. Для класса Rectangle определен в свою очередь подкласс SolidRectangle. Предположим, что в классе DisplayItem определена переменная экземпляра theCenter (задающая координаты центра изображения), а также следующие операции:
• draw - нарисовать изображение;
• move - передвинуть изображение;
• location - вернуть координаты изображения.
Операция location является общей для всех подклассов и не требует обязательного переопределения. Однако, поскольку только подклассы могут знать, как их изображать и передвигать, операции draw и move должны быть переопределены.
Класс Circle имеет переменную theRadius и соответственно операции для установки (set) и чтения значения этой переменной. Для этого класса операция draw формирует изображение окружности заданного радиуса с центром в точке theCenter. В классе Rectangle есть переменные theHeight и theWidth и соответствующие операции установки и чтения их значений. Операция draw в данном случае формирует изображение прямоугольника заданной высоты и ширины с центром в заданной точке theCenter. Подкласс SolidRectangle наследует все особенности класса Rectangle, но операция draw в этом подклассе переопределена. Сначала вызывается draw вышестоящего класса, а затем полученный контур заполняется цветом.
Теперь рассмотрим следующий фрагмент программы:
DisplayItem* items[10]; for (unsigned index = 0; index < 10; index++) items[index]->draw();
Вызов draw требует полиморфного поведения. У нас есть разнородный массив объектов, содержащий указатели на любые разновидности DisplayItem. Пусть некоторый клиент хочет, чтобы они все изобразили себя на экране. Наш подход - перебрать элементы массива и послать каждому указываемому объекту сообщение draw. Компилятор не может определить, какую функцию и откуда надо при этом вызвать, так как невозможно предсказать, на что будут указывать элементы массива во время выполнения программы. Посмотрим, как эта задача будет решаться в разных объектно-ориентированных языках.
Поскольку в Smalltalk нет типов, методы вызываются строго динамически. Когда клиент посылает сообщение draw очередному получателю, происходит следующее:
• получатель ищет селектор сообщения в словаре методов своего класса;
• если метод найден, то запускается его код;
• если нет, поиск производится в словаре методов суперкласса.
Таким образом, поиск распространяется вверх по иерархии и заканчивается на классе object, который является "предком" всех классов. Если метод не найден и там, посылается сообщение doesNotUnderstand, то есть, генерируется ошибка.
Главным действующим лицом в этом алгоритме является словарь методов. Он формируется при создании класса и, являясь частью его реализации, скрыт от клиентов. Вызов метода в Smalltalk требует примерно в 1.5 раза больше времени, чем вызов простой подпрограммы. В коммерческих версиях Smalltalk вызов методов ускорен на 20-30% за счет кеширования доступа к словарю [31].
Операция draw в подклассе solidRectangle представляет собой особый случай. Мы уже отмечали, что вначале вызывается одноименный метод суперкласса Rectangle. В Smalltalk для вызова метода суперкласса используется ключевое слово super. Поиск метода super draw начинается сразу с суперкласса.
Исследования Дейча дают основание полагать, что полиморфизм в 85% случаев не нужен, так что вызов метода часто можно свести к обычному вызову процедуры [32]. Дафф замечает, что в таких ситуациях программист часто делает неявные предположения, которые бы позволили раннее связывание [33]. К сожалению, в нетипизированных языках у него нет способа сообщить об этом компилятору.
В более строго типизированных языках, таких как C++, такой способ есть. В этих языках алгоритм вызова методов несколько отличается от описанного выше и позволяет сократить во многих случаях время поиска, сохранив при этом свойства полиморфизма.