Полиморфизм, вторая частьЛюбые операции, заданные уровнем выше в иерархии наследования, также будут доступны через объекты подтипов. Например, как было отмечено чуть ранее, операция сложения должна быть доступна для любых чисел, дробных и целых. При этом сама процедура проходит по-разному для целых и для дробных чисел. Перед разработчиком стоят две задачи: реализовать операцию сложения для соответствующих типов чисел и убедиться, что каждый новый тип, добавляемый в иерархию чисел, также будет реализовывать эту операцию обязательно. Для чего это надо на практике? Чтобы понять первопричину этих требований посмотрите на следующий пример кода.
Здесь запись
К примеру, если оба аргумента будут целыми числами, то в качестве операции сложения будет выбрана реализация для целых чисел, и ее результатом тоже будет целое число. Аналогично это сработает для двух дробных чисел. Но вот интересный вопрос: какая операция будет выбрана для случая, в котором аргумент
Если смотреть с конца, картина немного проясняется. Так как последняя строка в функции возвращает целое число (0), то результат сложения аргументов
Интересней дело обстоит с аргументом
Теперь внесем в нее еще одно изменение.
Здесь аргумент Обратите внимание, что типы данных функции не стали динамическими в классическом смысле этого слова. Динамические системы типов в языках программирования как раз допускают некоторую двоякость при обращении с переменными (например, одной и той же переменной можно присваивать значения разных типов в разных инструкциях при условии, что последующие операции учитывают этот факт). С++ является языком со статической системой типов, а это значит, что компилятор должен быть уверен в правильности написанного кода для всех участвующих в том или ином выражении значений. Примеры выше возможны в С++ благодаря системе выявления типов2 переменных. Выявление типов позволяет писать программы, не задавая явно типы данных для переменных. Хорошо спроектированная система типов в языке позволяет не отвлекаться на это. Правда, на программиста накладывается ответственность за правильность алгоритмов в рамках такой системы, так как необходимо заранее предвидеть возможные варианты трактовки системой типов того или иного выражения. Это может немного ограничить количество вариантов решения той или иной задачи. С другой стороны, использование системы неявных типов вовсе не обязательно. С помощью такой системы часто реализуется универсальный полиморфизм – способ реализации функций, поведение которых не привязано к типам данных переданных аргументов. В С++ нет полноценной системы выявления типов аргументов3, поэтому в данном языке широко используется полиморфизм подтипов. Для этого существует несколько инструментов: наследование и шаблоны типов.
С помощью наследования полиморфизм реализуется через отношение дочерних типов данных к базовому типу. Базовый тип данных играет роль полиморфного типа, то есть, такого типа, который может принимать разные формы в зависимости от типов переданных аргументов, аналогично переменным типам
Первое, что может вызвать вопросы – ключевое слово Другой момент – присваивание нуля прототипу функции. Это помечает функцию как чисто виртуальную, что делает сам класс абстрактным5. Если вспомнить пример иерархии классов в языке Smalltalk, упомянутый в предыдущей части, там говорилось о том, что корень всего дерева типов является абстрактным классом, потому что он не инициализируется как объект какого-то родительского класса. По этой же схеме в других языках программирования абстрактные классы помещают в корень иерархии для того, чтобы они служили описанием всех методов, доступных в каждом из объектов иерархии – то есть, описанием интерфейса. В некоторых языках такие классы выделяют в отдельную категорию “интерфейсов” [Whitney(b)].
Наконец, ключевое слово
Обратите внимание, что первый аргумент представлен ссылкой на базовый тип
Подведем промежуточные итоги. Чтобы понять принципиальную разницу между двумя основными видами полиморфизма нужно иметь ввиду, что универсальный полиморфизм позволяет использовать один и тот же код для аргументов любого типа, тогда как ad hoc полиморфизм может использовать разный код для аргументов разных типов. Какой из видов полиморфизма использовать, как правило, диктует характер поставленной перед программистом задачи. Например, подумайте над следующим вопросом: если перед вами стоит задача определить длину произвольного массива, имеет ли для вас значение каким типом данных представлен каждый элемент массива? Ответив на этот вопрос можно определить то, каким видом полиморфизма можно было бы воспользоваться для ее решения. УпражненияВернемся к проблеме сложения двух полиморфных типов. Используя модель, представленную в предыдущей главе, можно определить основные требования к классу “число”. Первое, базовый класс не может быть объектом, поэтому нужно убедиться, что компилятор не разрешает создавать переменные такого типа. Второе, все дочерние классы должны включать в себя определенные операции (которые составляют интерфейс класса “число”). Третье, полиморфные функции и методы, работающие с числами, должны принимать любое число, дробное или целое. Учитывая эти требования можно написать первую версию класса “число”.
В С++ абстрактный класс, реализованный через структуру данных, требует включения в него чистой виртуальной функции. Такая функция обязательно должна возвращать ссылку или указатель, потому что объект абстрактного класса возвращать будет невозможно, и возвращаемое значение будет к базовому типу приведено.
Целые числа можно представить абстрактным классом, так как инициализировать целое число без информации о знаке смысла не имеет. Но в нашей упрощенной модели класса отрицательных чисел нет, поэтому перегруженный оператор должен быть отмечен ключевым словом override. На основании этого класса создается класс положительных целых чисел.
Если перед программой будет стоять выбор: какую версию метода
Например, подумайте о том, какого типа будут переменные Сноски1Читается как “альфа”. 2Type inference – общее название семейства алгоритмов, которые частично или полностью определяют типы всех переменных на основании уже определенных ранее типов [Clarkson]. 3На момент написания данного текста. 4Это важный момент, потому что он имеет серьезные последствия; наследование позволяет обращаться к данным базового класса через дочерний класс, что означает возможность приводить объекты дочернего типа к базовому; из этого следует что можно изначально объявить переменную как базовый тип, а присвоить ей значение дочернего типа, но только если переменная является ссылкой или указателем, потому что компилятору заранее должен быть известен размер переменной в байтах, и только для указателей и ссылок размер не зависит от типа значения. 5Абстрактный базовый класс (ABC, англ.) – класс или структура, которая не может быть использована для создания переменных.
Copyright © 2023 Брынзан, Л. В. |