Книга: Основы объектно-ориентированного программирования

ОО-разработка и перегрузка

ОО-разработка и перегрузка

Анализ роли имен, сделанный в этой лекции, позволяет вернуться к вопросу о внутриклассовой перегрузке (in-class name overloading).

Напомню, что в таких языках, как Ada 83 и Ada 95, перегрузка разрешена - можно давать одно имя разным компонентам одного синтаксического модуля. Например, в одном пакете возможны определения:

infix "+" (a, b: VECTOR) is...
infix "+" (a, b: MATRIX) is...

Языки Java и C++ позволяют делать то же самое в пределах класса.

Ранее мы называли эту возможность синтаксической перегрузкой. Это - статический механизм. Для однозначного разрешения вызова, например, x + y, достаточно посмотреть на тип аргументов x и y, который очевиден из текста программы.

В объектной технологии применяется и более мощный механизм семантической (или динамической) перегрузки. Так, если классы VECTOR и MATRIX наследуют от общего предка NUMERIC компонент

infix "+" (a: T) is...

и каждый из них переопределяет его нужным образом, то понять, о какой операции + идет речь в выражении x + y, можно только динамически во время выполнения программы. Семантическая перегрузка - действительно интересный механизм, позволяющий использовать единое имя в тексте различных классов для представления разных вариантов по сути одной и той же операции, такой, как сложение в NUMERIC. Правила для утверждений, рассматриваемые в следующей лекции, уточнят эту ситуацию, требуя, чтобы переобъявления компонента сохраняли его фундаментальную семантику.

Сохраняется ли роль синтаксической перегрузки в объектной технологии? Трудно найти разумные аргументы в ее поддержку. Можно понять, почему язык Ada 83, не имеющий классов, ее использовал. Но в ОО-языке выбор одного имени для обозначения разных операций - это прямой путь к созданию беспорядка.

Проблема состоит еще и в том, что синтаксическая форма перегрузки вступает в конфликт с семантической, в активе которой - полиморфизм и динамическое связывание. Рассмотрим вызов x.f (a). Если он следует за полиморфными операторами присваивания x := y и a := b, то при сохранении имен его результат будет в точности тем же, что и для y.f (b), даже если типы b и y отличны от типов a и x. Но при перегрузке это свойство не сохраняется! Теперь f может быть перегруженным именем двух разных компонентов: одного - типа a, другого - типа b. Чему отдать предпочтение: синтаксической перегрузке или динамическому связыванию? Хуже того, базовый класс типа y может переопределять один или оба перегруженных компонента. И таким комбинациям, как и причинам ошибок, нет числа.

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

Таковы риски синтаксической перегрузки, а каковы все же ее плюсы? Ответить на этот вопрос нелегко. Простой принцип доступности кода гласит, что в тексте одного модуля читатель должен быть совершенно уверен в соответствии имени и значения. При внутриклассовой перегрузке это свойство теряется.

Типичный пример, иногда приводимый в подтверждение полезности перегрузки, связан с компонентами класса STRING. Чтобы к одной строке, при отсутствии перегрузки, добавить другую строку или отдельный символ, используются разные имена компонентов: s1.add_string (s2) и s1.add_character ('A'), или в инфиксной записи s := s1++ s2 и s := s1 + 'A'. При перегрузке обе операции можно назвать одинаково. Так ли это необходимо? Объекты типов CHARACTER и STRING наделены совершенно разными свойствами. Добавление символа всегда увеличивает длину строки на 1. Сцепление строк может оставить длину неизменной (если вторая строка пуста) или увеличить ее произвольным образом. Применение разных имен кажется не только разумным, но и желательным, особенно потому, что приведенные выше примеры ошибок действительно вполне возможны.

Предположим, даже, что решено использовать перегрузку, но и в этом случае придется подумать о более точном критерии, позволяющем выбирать нужный компонент. Общепринятый критерий синтаксической перегрузки различает компоненты по их сигнатуре, что не исключает неоднозначности. Типичный пример - процедуры создания точек в полярной или декартовой системе координат: make_cartesian и make_polar. Сигнатуры обеих процедур одинаковы, - они имеют два аргумента типа REAL, однако, работают совершенно по-разному. Перегрузку здесь использовать нельзя. Для отражения того факта, что оба компонента и в самом деле различны, им следует дать разные имена.

Реализацию процедур создания ("конструкторов") в Java и C++ нельзя описывать без иронии. Так, вы не вправе давать конструкторам разные имена, а вынуждены полагаться на перегрузку. Пытаясь решить эту проблему, я не нашел ничего лучше, чем ввести искусственный третий параметр.

В итоге (внутриклассовая) синтаксическая перегрузка в ОО-среде создает немало проблем, не давая видимых преимуществ. (Тем же, кто использует Java, C++ или Ada 95, можно посоветовать полностью отказаться от перегрузки, прибегая к ней лишь при создании конструкторов, то есть тогда, когда язык не оставляет другого выбора.) Стараясь умело применять объектный подход, придерживайтесь простого правила: каждый компонент имеет имя, каждое имя означает только один компонент.

Оглавление книги


Генерация: 0.050. Запросов К БД/Cache: 0 / 0
поделиться
Вверх Вниз