Россия, Москва |
Лекция 10: Концепция полиморфизма и ее реализация в языке C#
Формализуем понятие полиморфизма применительно к объектно-ориентированному подходу. Напомним, что история развития теоретических основ полиморфизма изложена во вступительной лекции.
Под полиморфизмом будем иметь в виду возможность оперирования объектами без однозначной идентификации их типов.
Напомним, что понятие полиморфизма уже исследовалось в первой части курса, посвященной функциональному подходу к программированию.
Наметим концепции, объединяющие функциональный и объектно-ориентированный подходы к программированию с точки зрения полиморфизма.
Как было отмечено в ходе исследования функционального подхода к программированию, концепция полиморфизма предполагает в части реализации отложенное связывание переменных со значениями. При этом во время выполнения программы происходят так называемые "ленивые" или, иначе, "замороженные" вычисления. Таким образом, означивание языковых идентификаторов выполняется по мере необходимости.
В случае объектно-ориентированного подхода к программированию теоретический и практический интерес при исследовании концепции полиморфизма представляет отношение наследования, прежде всего, в том смысле, что это отношение порождает семейства полиморфных языковых объектов.
С точки зрения практической реализации концепции полиморфизма в языке программирования C# в форме полиморфных функций особое значение для исследования имеет механизм интерфейсов.
Напомним, что реализация полиморфизма при функциональном подходе к программированию основана на оперировании функциями переменного типа.
Для иллюстрации исследуем поведение встроенной SML-функции hd (от слова "head" - голова), которая выделяет "голову" (первый элемент) списка, вне зависимости от типа его элементов. Применим функцию к списку из целочисленных элементов:
hd [1, 2, 3]; val it = 1: int
Получим, что функция имеет тип функции из списка целочисленных величин в целое число:
int list -> int
В случае списка из значений истинности та же самая функция
hd [true, false, true, false]; val it = true: bool
возвращает значение истинности, т.е. имеет следующий тип:
bool list -> bool
Наконец, для случая списка кортежей из пар целых чисел
hd [(1,2)(3,4),(5,6)]; val it = (1,2) : int*int
((int*int)list -> (int*int))
В итоге можно сделать вывод, что функция hd имеет тип
(type list) -> type
где type - произвольный тип, т.е. функция hd полиморфна.
Проиллюстрируем сходные черты и особенности реализации концепции полиморфизма при функциональном и объектно-ориентированном подходе к программированию следующим фрагментом программы на языке C#:
void Poly(object o) { Console.WriteLine(o.ToString()); }
Как видно, приведенный пример представляет собой описание полиморфной функции Poly, которая выводит на устройство вывода (например, на экран) произвольный объект o, преобразованный к строковому формату ( o.ToString() ).
Рассмотрим ряд примеров применения функции Poly:
Poly(25); Poly("John Smith"); Poly(3.141592536m); Poly(new Point(12,45));
Заметим, что независимо от типа аргумента (в первом случае это целое число 25, во втором - символьная строка "John Smith", в третьем - вещественное число π=3.141592536, в четвертом - объект типа Point, т.е. точка на плоскости с координатами (12,45) ) обработка происходит единообразно и, как и в случае с языком функционального программирования SML, функция генерирует корректный результат.
Как видно из приведенных выше примеров, концепция полиморфизма одинаково применима как к функциональному, так и к объектно-ориентированному подходу к программированию. При этом целью полиморфизма является унификация обработки разнородных языковых объектов, которые в случае функционального подхода являются функциями, а в случае объектно-ориентированного - объектами переменного типа. Отметим, что для реализации полиморфизма в языке объектно-ориентированного программирования C# требуется четкое представление о ряде понятий и механизмов.
Естественно, говорить о полиморфизме можно только с учетом понятия типа. Типы определяют интерфейсы объектов и их реализацию. Переменные, функции и объекты в изучаемых в данном курсе языках программирования также рассматриваются как типизированные элементы. Необходимо также напомнить, что важное практическое значение при реализации полиморфизма в языке C# имеет механизм интерфейсов (под которыми понимаются чисто абстрактные классы с поддержкой полиморфизма, содержащие только описания без реализации ). Для реализации концепции множественного наследования необходимо принять ряд дополнительных соглашений об интерфейсах.
Сложно говорить о полиморфизме и в отрыве от концепции наследования, при принятии которой классы и типы объединяются в иерархические отношения частичного порядка из базовых классов (или, иначе, надклассов) и производных классов (или, иначе, подклассов). При этом существуют определенные различия между наследованием интерфейсов как частей, отвечающих за описания классов, и частей, описывающих правила реализации.
Еще одним значимым механизмом, сопряженным с полиморфизмом, является так называемое отложенное связывание (или, иначе, "ленивые" вычисления ), в ходе которых значения присваиваются объектам (т.е. связываются с ними) по мере того как эти значения требуются во время выполнения программы.
Напомним классификацию стратегий вычислений, построенную в первой части курса, посвященной исследованию функционального подхода к программированию.
При вычислении с вызовом по значению (call-by-value) все выражения должны быть означены до вычисления операции аппликации. Заметим, что формализация стратегии вычислений с вызовом по значению возникла в числе первых моделей computer science в виде абстрактной SECD-машины П.Лендина.
При вычислении с вызовом по имени (call-by-name) до вычисления операции аппликации необходима подстановка термов вместо всех вхождений формальных параметров до означивания . Стратегию вычислений с вызовом по имени иначе принято называть вызовом по ссылке (call-by-reference).
Наконец, при вычислении с вызовом по необходимости (call-by-need) ранее вычисленные значения аргументов сохраняются в памяти компьютера только в том случае, если необходимо их повторное использование. Именно эта стратегия лежит в основе "ленивых" (lazy), "отложенных" (delayed) или "замороженных" (frozen) вычислений, которые принципиально необходимы для обработки потенциально бесконечных структур данных.