Текст опубликован в личном блоге и его автор не имеет отношения к администрации сайта
В предыдущей статье (18-й в этом цикле) – мы подробно рассматривали простые декларации классов, создание экземпляров этих классов и работу с ними. Рассмотрим теперь и производные классы. Как понятно из названия, они уже создаются не "с чистого листа" – а на основе другого, ранее уже декларированного класса. Для производного класса – этот класс называется "классом-родителем" (или "суперклассом"). Всё, что уже присутствует в суперклассе – и его компоненты, и его методы – автоматически становится доступным и в производных классах: наследуется ими от родителя.
Декларация производного класса отличается от уже рассмотренных нами деклараций классов только одним элементом: перед именем нового класса – ставится имя суперкласса в квадратных скобках:
!! [SuperClassName] NewClassName (ArgA ArgB ArgC ...) : [LocA LocB ...] {
...
! MethodA = ( ... ),
! MethodB = ( ... ),
! MethodC = ( ... ),
...
};
В этом примере, мы декларируем новый класс NewClassName, со своими аргументами (ArgA, ArgB, ArgC, ...), компонентами (LocA, LocB ...) и (внутренними) методами (MethodA, MethodB, MethodC ...). Но, кроме всего этого – в новом классе будет доступно и всё, что уже было в классе SuperClassName: все его компоненты – автоматически становятся компонентами NewClassName, а все его методы – будут полностью применимы и к экземплярам NewClassName. Как результат этого, класс NewClassName наследует весь уже имеющийся функционал от SuperClassName.
Механизм наследования в ООП – изобретён в основном для того, чтобы предоставить программисту возможность явно выразить какой-то общий функционал группы сходных классов. Рассмотрим, например, условную программу – автомобильный симулятор. Практически точно можно сказать, что в ней будет нужен базовый суперкласс (GenericCar), в котором реализована общая функциональность для всех автомобилей в программе. У всех автомобилей есть много общего (топливные баки, двигатель, трансмиссия, четыре колеса ...) – и весь связанный с этим код лучше всего реализовать именно в классе GenericCar, чтобы не писать его многократно. С другой стороны – автомобили, всё-таки, совсем неодинаковые. Даже очень далёкий от автомобильного мира человек – легко отличит тяжёлый грузовик от гоночного болида, и ему даже не потребуется для этого лезть под капот. Для того, чтобы реализовать специфичность конкретных классов автомобилей – и требуются производные от GenericCar классы.
Любой суперкласс может порождать любое количество подклассов: ограничений тут не существует. Глубина вложенности в иерархии классов также практически ничем не ограничена: любой производный класс – сам может быть суперклассом для других производных классов (и так далее, на любую глубину). В языке, однако, нет множественного наследования (производный класс может иметь только один суперкласс).
Приведём (пока, довольно искусственный) пример иерархии классов:
!! Class_1 (A B C) {};
!! [Class_1] Class_12 (K L M N) {};
!! [Class_12] Class_123 (X Y Z) {};
Здесь Class_1 – это корень всей иерархии, Class_12 – производный от него, а Class_123 – производный уже от Class_12. Поэтому, в классе Class_12 – помимо собственных аргументов (K, L, M, N) доступны и аргументы суперкласса (A, B, C), а последнем классе – доступны все перечисленные, как и собственные аргументы (X, Y, Z). Для простоты примера, ни один из этих классов не декларирует, например, свои методы (но если бы они были – они наследовались бы вместе со всем прочим).
А как создать экземпляры этих классов?
Object1 = Class_1 (10, 20, 30); Object12 = Class_12 (100, 200, 300)::(99, 88, 77, 66); Object123 = Class_123 ((1001, 2002, 3003)::(909, 808, 707, 606))::(3, 2, 1);
Для корневого Class_1 – сюрпризов нет: мы просто передаём значения трём его компонентам. С производными классами – всё интереснее. Каждый производный класс имеет два набора аргументов: один – свой собственный, а второй – для своего суперкласса! В списке его аргументов – начальным аргументом всегда тоже является список (аргументы для суперкласса), а все прочие уже передаются непосредственно классу. Или (если вспомнить про внутреннюю структуру списков): "головой" (l_head) для списка аргументов будут аргументы для суперкласса, а его "хвостом" (l_tail) – аргументы уже для производного класса. Поэтому, для записи списка аргументов производных классов – удобно использовать бинарную операцию "::", явно конструирующую единый список из "головы" и "хвоста". Именно так мы и делаем. Конечно, всё это чистой воды синтаксическое "украшательство". Оба списка аргументов могут быть записаны и в более традиционном виде (но с операций "::" – всё намного нагляднее):
Object12 = Class_12 ((100, 200, 300), 99, 88, 77, 66); Object123 = Class_123 (((1001, 2002, 3003), 909, 808, 707, 606), 3, 2, 1);
Ещё один важный нюанс состоит в том, что если существует суперкласс, то аргументы для суперкласса – в вызове должны присутствовать всегда. И даже когда явных аргументов у суперкласса просто нет – необходимо явно передать суперклассу пустое значение () вместо списка аргументов! (В противном случае – аргументом для супекласса будет первый аргумент для производного класса, все остальные также будут переданы производному классу со сдвигом, и в результате получится полный хаос.) Чтобы избежать подобных ошибок – при создании экземпляров производных классов надо быть осторожнее.
Также, нередко полезно проверять содержимое создаваемых объектов. Уже говорилось в предыдущей статье, что вызов x_vars вполне применим и к классам. Однако, для производного класса – он выводит только список его собственных компонент, игнорируя компоненты суперкласса (или суперклассов). Впрочем, нетрудно реализовать более удобный инструмент – но для этого полезно следующее:
|
super_of (ClassRef)
|
Ссылка на суперкласс для класса ClassRef (или (), если такого нет)
|
Полагаю, что делает этот функтор ясно из описания. Например, для трёх наших классов:
С помощью super_of нетрудно написать функтор, который будет выводить содержимое объекта производного класса целиком и полностью (заметим, что для этого он вызывает себя рекурсивно):
! all_class_vars (ClassRef) = {
if (super_of (ClassRef) [<>] ())::
all_class_vars (super_of (ClassRef));
x_vars (ClassRef);
};
! dump_object (Object) = Object.all_class_vars (class_of (Object));
Теперь посмотрим полный дамп содержимого трёх созданных объектов:
dump_object (Object1); dump_object (Object12); dump_object (Object123);
Что выводит:
A = 10; B = 20; C = 30; A = 100; B = 200; C = 300; K = 99; L = 88; M = 77; N = 66; A = 1001; B = 2002; C = 3003; K = 909; L = 808; M = 707; N = 606; X = 3; Y = 2; Z = 1;
Работа с производными классами, в принципе, не отличается от работы с корневыми – но тут тоже есть свои нюансы. Например, префикс класса ("!!") – также работает и с производными классами. Но для них, он автоматически "подключает" не только сам класс – но и все его суперклассы (вплоть до самого верхнего в иерархии):
Примерно также всё и с префиксом объектов. Функтор with (он же, бинарная операция ".") – применим к объектам производных классов. Но для них он (как обычно, временно!) делает свой первый операнд текущим экземпляром не только для своего класса, но и для всех его суперклассов, вплоть до самого "верхнего". Что логично, поскольку все определения суперкласса – доступны и в производных классах. Например:
Другими словами, чем глубже производный класс находится в иерархии – тем для большего количества классов его объекты становятся текущими экземплярами. (Что также логично: ведь каждый объект принадлежит не только собственному классу, но и всем его суперклассам, если они есть.) Ещё раз подчеркнём, что все эти изменения – временные, и действуют только на время выполнения (или вычисления) выражения Expr. По его завершении, все текущие экземпляры для всех затронутых классов восстанавливают свои прежние значения!
Все атрибуты класса-родителя (а вместе с ними, и вся его функциональность) – наследуются строго сверху вниз. Важно то, что производный класс может не только заимствовать функциональность от всех своих родителей – но и изменять её (в определённых, строго заданных пределах). В языке присутствует механизм для передачи функциональности "снизу вверх", который он даёт производным классам возможность повлиять на работу своих суперклассов (если это в них заранее предусмотрено): виртуальные методы суперклассов.
Каждый виртуальный метод – имеет одно название, но (как правило) разные реализации для суперкласса и для производных от него классов. При вызове такого метода – вызывается именно та его версия, которая актуальна для текущего экземпляра данного класса. Т.е., если это – экземпляр суперкласса, то вызывается именно его версия метода, но вот когда он уже является экземпляром какого-то из подклассов – будет вызвана версия, специфичная для данного подкласса. Именно таким образом работает механизм виртуализации вызовов, и он обеспечивает полиморфизм: возможность для объекта менять своё поведение, в зависимости от того, к какому именно из производных классов он принадлежит.
Чтобы в классе имелись виртуальные методы – их надо сперва декларировать. Это делается так:
!! MyClass (Arg1 Arg2 Arg3 ...) : [Field1 Field2 Field3 ...]
#{ Virtual1 Virtual2 Virtual3 ... }
{
...
! #Virtual1 = { ... },
! #Virtual2 = { ... },
! #Virtual3 = { ... }
...
};
Как здесь показано, декларация виртуальных методов является списком: в фигурных скобках, с префиксом "#", и (как и списки параметров и компонент) без явных разделителей. Она находится после заголовка класса, но до самого тела класса (т.е. списка декларируемых классом внутренних методов и пр). Внутри тела класса – все виртуальные методы уже доступны: их можно вызывать, получать на них ссылки и пр.
Однако, чтобы вызов виртуального метода имел реальный эффект, требуется не только его декларация – но и реализация. В примере выше – эти методы не только декларированы, но и реализованы для MyClass. Реализация виртуального метода синтаксически отличается от обычного метода только одним: перед его именем обязательно ставится префикс "#"! Во всём остальном, синтаксис виртуального метода (как и семантика) такой же, как и у любого другого: он также может иметь параметры, локальные переменные, и (обязательно) имеет тело, описывающее все выполняемые им действия.
Главное отличие виртуальных методов от "традиционных" – состоит в том, что в любом классе, производном от MyClass, они могут иметь и собственную реализацию (обычно отличающуюся от реализации в суперклассе). Декларация виртуального метода должна быть только одной – но вот реализаций может быть столько, сколько имеется производных классов. Производный класс может переопределять любой унаследованный от предков виртуальный метод – хотя, он может этого и не делать. В последнем случае, он просто наследует реализацию этого метода от своего суперкласса. Разумеется, каждый производный класс может переопределять унаследованный виртуальный метод только один раз: попытки переопределить его несколько раз считаются ошибкой.
Хотя в приведённом примере класс MyClass реализует все свои виртуальные методы – но вообще-то суперкласс и это делать не обязан! Классы, в которых отсутствует явная реализация одного (или более) виртуальных методов – как правило, являются абстрактными. Создание экземпляров (то есть, инстанциация) – для абстрактных классов обычно не рекомендуется. Единственное, для чего эти классы полезны – для создания производных от них (более "конкретных") классов, в которых все декларируемые в суперклассе виртуальные методы уже реализованы.
Тем не менее, сейчас в языке нет явного способа декларировать "абстрактность" какого-то класса: она является не более, чем условностью. Экземпляры любого класса можно создавать (в том числе, и сугубо "абстрактного"). Даже не реализованные явно виртуальные методы – можно вызывать! Ничего страшного при этом не происходит (впрочем, и ничего интересного – тоже): вызванный метод не выполняет никаких действий, и возвращает (). Фактически, всякий виртуальный метод MyVirtual (не реализованный явно) – ведёт себя так, будто в декларировавшем его классе присутствовала такая реализация:
! #MyVirtual = ()
Тем не менее, на практике всё-таки лучше придерживаться определённой дисциплины: не создавать экземпляры "абстрактных" классов, не вызывать не реализованные явно виртуальные методы и т.д. (Возможно, что в дальнейших версиях языка это уже будет рассматриваться, как явная ошибка.)
На виртуальный метод (как и на любой другой) можно получить ссылку. При косвенном вызове виртуального метода – применяется тот же "механизм виртуализации", что и при прямом. Имеется и предикат, проверяющий метод на "виртуальность":
|
is_virtual (FuncRef)
|
Истина (1), если FuncRef – ссылка на виртуальный метод (любого класса)
|
В противном случае, вызов is_virtual возвращает 0. Если метод является виртуальным, то можно узнать, какой именно класс является для него оригинатором (т.е., где именно он был декларирован):
|
originator (FuncRef)
|
Если FuncRef – ссылка на виртуальный метод, то возвращается ссылка на его класс-оригинатор
|
В противном случае, будет сообщение об ошибке, а результатом будет ().
Рассмотрим теперь несколько более практичных примеров наследования. Вот пример, который содержит практически любое руководство по объектно-ориентированному программированию – "зоопарк" с разными животными.
!! Animal (Name) : []
#{ whatis voice }
{
! greet = <: (whatis (), " ", Name, " говорит вам: ", voice (), "\n")
};
!! [Animal] Dog {
! #whatis = "Пес",
! #voice = "Гав-гав!"
};
!! [Animal] Cat {
! #whatis = "Кот",
! #voice = "Мяя-яу!"
};
!! [Animal] Rat {
! #whatis = "Крыса",
! #voice = "(громко пищит)"
};
!! [Animal] Cow {
! #whatis = "Корова",
! #voice = "Муу-уу!!"
};
MyZoo = (
Cow ("3орька"),
Dog ("Бобик"),
Dog ("Барбос"),
Cat ("Мурзик"),
Cat ("Барсик"),
Rat ("Лариска")
);
l_loop (animal, MyZoo, Animal!!animal.greet ());
Класс Animal – это (фактически) абстрактный суперкласс для всех описанных далее "животных". Виртуальный метод whatis – определяет видовое имя ("Пёс", "Кот" и пр.), а виртуальный метод voice – заставляет животное "подать голос". Ещё в суперклассе есть метод greet, заставляющий животное вас поприветствовать! При этом, сам по себе этот метод – не виртуальный: в производном классе явно переопределить его нельзя! Однако, он вызывает виртуальные методы whatis и voice – так что, в этом примере все животные приветствуют вас по-разному:
Корова 3орька говорит вам: Муу-уу!! Пес Бобик говорит вам: Гав-гав! Пес Барбос говорит вам: Гав-гав! Кот Мурзик говорит вам: Мяя-яу! Кот Барсик говорит вам: Мяя-яу! Крыса Лариска говорит вам: (громко пищит)
Все эти методы дают ожидаемый эффект, учитывающий, что они реализованы для каждого производного класса по-своему. Конечно, это очень тривиальный пример: результатом для всех виртуальных методов – является константная строка. А вот пример поинтереснее: геометрические фигуры (для которых уже кое-что реально вычисляется: периметр и площадь).
` (Абстрактный) геометрический объект `
!! Shape
# {
describe ` информация о фигуре, в виде списка `
perimeter ` вычислить периметр `
area ` вычислить площадь `
} {};
` Прямоугольник (со сторонами A и B) `
!! [Shape] Rectangle (A B) {
! #describe = ['Прямоугольник (' A ' * ' B ')'],
! #perimeter = 2*(A + B),
! #area = A*B
};
` Квадрат (со стороной S) `
!! [Shape] Square (S) {
! #describe = ['Квадрат [сторона ' S ']'],
! #perimeter = 4*S,
! #area = S*S
};
` Круг (с радиусом R) `
!! [Shape] Circle (R) {
! #describe = ['Круг [радиус ' R ']'],
! #perimeter = 2*pi(R),
! #area = pi(R*R)
};
` Треугольник (со сторонами A, B, C) `
!! [Shape] Triangle (A B C) {
! #describe = ['Треугольник (' A ',' B ',' C ')'],
! #perimeter = A + B + C,
! #area : [p] = {
` (формула Герона:) `
p = (A + B + C) / 2;
sqr (p*(p - A)*(p - B)*(p - C))
}
};
Абстрактный суперкласс Shape представляет "фигуру вообще", обладающую лишь теми свойствами, что у неё есть некоторое описание (describe), и для неё могут быть вычислены периметр (perimeter) и площадь (area). Далее, мы уже реализуем конкретные разновидности геометрических фигур. Мы даже можем для удобства собрать их в список:
Figures = ( Square ()::(5), Rectangle ()::(6, 9), Circle ()::(10), Triangle ()::(5, 12, 13) ); l_loop (figure, Figures, <: Shape!! figure. (describe (), ": ", "периметр = ", perimeter (), "; ", "площадь = ", area (), "\n") );
Заметьте, что здесь при создании каждой "фигуры", мы передаём ей начальный аргумент (): исключительно для суперкласса Shape (у которого нет своих аргументов). В результате, получим:
Квадрат [сторона 5]: периметр = 20; площадь = 25 Прямоугольник (6 * 9): периметр = 30; площадь = 54 Круг [радиус 10]: периметр = 62.831853; площадь = 314.15927 Треугольник (5,12,13): периметр = 30; площадь = 30.
Наследование – механизм очень мощный! Абстракции, которые предоставляют суперклассы, полезны в самых разных ситуациях – от работы с файловой системой и сетевыми соединениями, до основанного на "виджетах" интерфейса с графической средой (про который будем говорить предметно уже в следующих статьях).