Глава     13


Разделы

Содержание

Выше во время наших рассуждений мы предполагали, что класс наследует только от одного родительского класса. Хотя эта ситуация является, безусловно, типичной, тем не менее бывают случаи, когда некоторая абстракция логически вытекает из двух (или более) независимых источников. Если вы представляете себе классы как аналоги категорий, как мы делали в главе═1, и пытаетесь описать себя в терминах групп, к которым принадлежите, то весьма вероятно, что вы построите много непересекающихся классификаций. Например, я═≈ отец ребенка, профессор, гражданин США. Ни одна из этих категорий не является собственным подмножеством другой.

Еще пример. Бет занимается художественной лепкой. Ее мы относим к классу Potter. Ее соседка Маргарет рисует портреты, она═≈ PortraitPainter. Тот тип живописи, которым она занимается, отличен от ремесла Пола: он═≈ маляр (HousePainter). Обычно мы рассматриваем однонаправленное наследование как способ специализации (в данном примере Potter═≈ это частный случай художника Artist). Однако множественное наследование следует рассматривать как комбинирование (портретист PortraitPainter═≈ это творческий человек Artist и художник Painter).

13.1. Комплексные числа

Разделы

Мы проиллюстрируем трудности, возникающие при одиночном наследовании, на более конкретном примере. В языке Smalltalk класс Magnitude определяет некий протокол для объектов с определенной мерой: они могут сравниваться друг с другом по величине 12 . Например, отдельные символы (экземпляры класса Char) сравниваются по своей внутренней кодировке (скажем, ASCII). Более традиционный класс сравнимых объектов═≈ числа, то есть экземпляры класса Number в терминах Smalltalk. Помимо сравнения, экземпляры класса Number поддерживают выполнение арифметических операций (сложение, умножение и═т.═д.). Эти операции не имеют смысла для объектов класса Char. В Smalltalk имеется несколько типов чисел: целые (Integer), дробные (Fraction), вещественные (Float).

Комментарий
12 Мера═≈ это нечто большее, чем просто способность сравниваться друг с другом, а зачастую и просто нечто отличное от отношения ╚больше√меньше╩ (например, для рассматриваемых автором далее комплексных чисел характеристика меры определена, а отношение сравнения═≈ нет).═≈ Примеч. перев.

Предположим теперь, что мы добавляем класс Complex, который представляет собой абстракцию комплексного числа. Арифметические операции, несомненно, определены для комплексных чисел. Разумно сделать Complex подклассом класса Number, так что арифметика наследуется и переопределяется. Проблема состоит в том, что сравнение двух комплексных чисел═≈ это нечто двусмысленное. Комплексные числа просто не сравнимы между собой.

Итак, мы имеем следующие ограничения:

Невозможно удовлетворить всем этим условиям с помощью иерархии одиночного наследования. Имеется несколько альтернативных решений данной проблемы:

  1. Сделать класс Complex подклассом класса Number, который в свою очередь является подклассом класса Magnitude. Затем переопределить методы, имеющие отношение к сравнению экземпляров в классе Complex, с тем, чтобы при их вызове генерировалось сообщение об ошибке. Это═≈ создание подкласса с ограничением, как описано в главе═7. Хотя такое решение и не является элегантным, оно иногда наиболее целесообразно, если ваш язык программирования не поддерживает множественное наследование.
  2. Не использовать наследование вообще. Определить заново каждый метод во всех классах Char, Integer, Complex и т.═д. Это решение иногда называется декомпозицией иерархического дерева. Естественно, оно устраняет все преимущества наследования, описанные в главе═7,═≈ например, многократное использование кода и гарантированность интерфейсов. Для языков программирования со статическими типами данных (таких, как C++ или Object Pascal) этот способ запрещает полиморфные объекты: например, нельзя создать переменную, которая содержала бы произвольный измеримый объект или число произвольного типа.
  3. Использовать часть иерархии наследования и имитировать оставшуюся ветвь. Например, можно поместить все числа в класс Number, а для каждого измеримого объекта (символа или числа) задать операцию сравнения.
  4. Сделать два класса Magnitude и Number независимыми друг от друга и в силу этого потребовать, чтобы класс Integer наследовал от них обоих при задании свойств (рис.═13.3). Класс Float будет аналогичным образом наследовать как от Number, так и от Magnitude.


Рис.═13.3. Иерархия множественного наследования для комплексных чисел

Важный момент в альтернативах═2 и═3═≈ это то, что они в гораздо большей степени привлекательны для языков программирования с динамическими типами данных (Objective-C, Smalltalk). В языках C++ или Object Pascal определение того, какие именно типы являются ╚измеримыми╩ или ╚сравнимыми╩, выражается в терминах классов. А именно объект ╚измерим╩, если он может быть присвоен переменной, объявленной с классом Magnitude. С другой стороны, в языках Smalltalk и Objective-C объект является ╚измеримым╩, если он понимает сообщения, относящиеся к сравнению объектов, независимо от того, в каком месте иерархии классов он находится. Тем самым, чтобы заставить комплексные числа взаимодействовать с другими объектами, даже если они не имеют общих классов-предков, может использоваться техника двойной диспетчеризации (см. работу [Ingalls═1986] или раздел═18.2.3).

О классе, который наследует от двух или более родительских классов, говорят, что он порожден множественным наследованием. Множественное наследование═≈ это мощное и полезное свойство языка программирования, но оно создает много изощренных и сложных проблем при реализации. Из рассматриваемых нами языков только C++ поддерживает множественное наследование, хотя некоторые исследовательские версии Smalltalk также обладают этим свойством. В данной главе мы будем изучать некоторые из преимуществ и проблем, связанных с множественным наследованием.

13.2. Всплывающие меню

Разделы

Второй пример проиллюстрирует многие моменты, которые необходимо иметь в виду, когда вы рассматриваете множественное наследование. Он вдохновлен библиотекой для создания графических пользовательских интерфейсов, которая связана с объектно-ориентированным языком Eiffel [Meyer═1988a, Meyer═1988b]. В этой системе меню описываются как класс Menu. Экземпляры класса Menu поддерживают такие свойства, как число пунктов меню, список команд и т.═д. Функционирование, связанное с меню, подразумевает способность отображать меню (то есть себя) на графическом экране и выбирать один из его пунктов (рис.═13.4).


Рис.═13.4. CRC-карточка для класса Menu

Каждый элемент (пункт) меню представляет собой экземпляр класса MenuItem. Экземпляры содержат текст элемента, ссылку на родительское меню и описание команды, которую надо выполнить при выборе этого пункта меню (рис.═13.5).


Рис.═13.5. CRC-карточка для класса MenuItem

Типичным средством пользовательского графического интерфейса являются многоуровневые меню (в некоторых системах они называются каскадными меню), которые требуются, когда элемент меню содержит несколько альтернативных команд. Например, пункт главного меню для эмулятора терминала может иметь имя ╚задать опции╩. Когда этот пункт (подменю) выбран пользователем, высвечивается второе меню, которое позволяет выбирать из набора имеющихся опций (темный/светлый фон и т.═д.).

Многоуровневое меню определенно принадлежит классу Menu. Оно содержит ту же информацию, что и класс Menu, и должно вести себя подобным образом. С═другой стороны, оно так же, несомненно, является элементом MenuItem, так как содержит имя и способно выполнить команду (вывести свой образ на экран), когда соответствующий пункт выбран в родительском меню. Требуемое поведение может быть достигнуто с минимальными усилиями, если мы разрешим классу WalkingMenu наследовать от обоих родителей. Например, когда всплывающему меню требуется выполнить действие по щелчку мыши (унаследованное от класса MenuItem), оно выводит на экран свое содержимое (вызывая графический метод, унаследованный от класса Menu).

Как и в случае с одиночным наследованием, при использовании множественного наследования важно иметь в виду условие ╚быть экземпляром╩. В нашем примере множественное наследование оправдано, поскольку имеет смысл каждое из утверждений: ╚подменю есть меню╩ и ╚подменю есть пункт меню╩. Когда отношение ╚быть экземпляром╩ не выполнено, множественное наследование может использоваться неправильно. Например, неверно описывать ╚автомобиль╩ как подкласс двух классов: ╚мотор╩ и ╚корпус╩. Точно так же класс ╚яблочный пирог╩ не следует выводить из классов ╚пирог╩ и ╚яблоко╩. Очевидно, что яблочный пирог есть пирог, но он не есть яблоко.

Когда множественное наследование используется правильным образом, происходит тонкое, но тем не менее важное изменение во взгляде на наследование. Интерпретация условия ╚быть экземпляром╩ при одиночном наследовании рассматривает подкласс как специализированную форму другой категории (родительского класса). При множественном наследовании класс является комбинацией нескольких различных характеристик со своими интерфейсами, состояниями и базовым поведением, специализированным для рассматриваемого случая. Операции записи и поиска объектов в неком хранилище (скажем, на жестком диске) представляют типичный пример. Часто эти действия реализованы как часть поведения, связанного с определенным классом типа Persistence или Storable. Чтобы придать такую способность произвольному классу, мы просто добавляем Storable к списку предков класса.

Конечно же, важно различать между наследованием от независимых источников и построением из независимых компонент, что иллюстрируется на примере ╚автомобиля╩ и ╚мотора╩.

13.3. Двусмысленность имен

Разделы

Часто возникающее затруднение при множественном наследовании состоит в том, что имена могут использоваться для обозначения более чем одной операции. Чтобы проиллюстрировать это, мы рассмотрим еще раз модель карточной игры. Предположим, что уже имеется абстракция карточной колоды CardDeck, которая обеспечивает надлежащую функциональность: тасование колоды (метод shuffle), выбор отдельной карты (метод draw 13 ) и т.═д., но графика при этом не реализована. Предположим далее, что другое множество классов обеспечивает поддержку обобщенных графических объектов. Они содержат поле данных (точку на плоскости) и, кроме того, виртуальный метод с именем draw для графического отображения самих себя.

Комментарий
13 Здесь используется тот факт, что в английском языке глагол draw кроме значения═╚рисовать╩ имеет также и массу других значений (в англо-русском словаре В.═К.═Мюллера их список занимает почти две колонки текста), среди которых имеются ╚отбрасывать╩, ╚вытаскивать╩ и ╚тянуть жребий╩.═≈ Примеч. перев.

Программист решает создать класс новой абстракции GraphicalDeck, которая наследует как от класса CardDeck, так и от GraphicalObject. Ясно, что концептуально класс GraphicalDeck является колодой карт CardDeck и тем самым логически выводится из нее. GraphicalDeck является также графическим объектом GraphicalObject. Единственная неприятность═≈ это двойное значение команды draw.

Как отмечает Мейер [Meyer═1988a], проблема однозначно кроется в дочернем классе, а не в родителях. Команда draw имеет недвусмысленное значение для каждого из родительских классов, когда они рассматриваются изолированно. Сложность состоит в их комбинировании. Поскольку загвоздка возникает на уровне дочернего класса, то и решение должно быть найдено здесь же. В данном случае дочерний класс обязан принять решение, как устранить двусмысленность перегруженного имени.

Решение проблемы обычно включает комбинацию переименования и переопределения. Под переопределением мы понимаем изменение в выполняемой операции или команде, как это происходит при модификации в подклассе виртуального метода. Под переименованием мы просто подразумеваем смену имени метода без изменения его функционирования. В случае колоды карт GraphicalDeck программист может приписать методу draw задачу рисования графического образа, а процесс вытаскивания карты из колоды переименовать в drawCard.

13.3.1. Наследование через общих предков

Двусмысленность имен

Более сложная проблема возникает, если программист хочет использовать два класса, имеющие общего родителя. Предположим, что программист разрабатывает набор классов для потоков ввода/вывода. Поток данных═≈ это обобщение понятия файла. Элементы первого могут быть более структурированы. Например, часто используются потоки целых чисел или потоки чисел с плавающей точкой. Класс InStream обеспечивает протокол для входных потоков. Пользователь может открыть входной поток путем присоединения его к файлу данных, выбрать очередной элемент из потока и т.═д. Класс OutStream обеспечивает похожую функциональность для выходных потоков. Оба класса наследуют от общего родителя с именем Stream. Информация, которая указывает на собственно файл данных, прячущийся под маской потока, содержится в родительском классе.


Рис.═13.6. Граф множественного наследования

Теперь предположим, что пользователь хочет создать комбинированный поток ввода/вывода InOutStream (рис. 13.6). Имеет смысл объявить его потомком и потока ввода, и потока вывода. Переименование (см. предыдущий раздел) позволяет принять решение по поводу любой функции, определенной одновременно в классах InStream и OutStream. Но что делать со свойствами, наследуемыми от общего прародителя Stream? Трудность состоит в том, что дерево наследования═≈ это направленный граф, а не просто дерево (см. рис.═13.6). Если методы═≈ это единственное, что наследуется от общего родительского класса, то может быть использована описанная выше техника разрешения противоречий. Но если родительский класс определяет также и поля данных (например, указатель на файл), то имеются два варианта. Хотим ли мы иметь две копии полей данных или только одну? Аналогичная проблема возникает, если прародительский класс использует конструкторы или подобные им средства инициализации, которые должны вызываться только однажды. В следующем разделе мы опишем, как с этой проблемой справляется язык C++.

13.4. Множественное наследование в C++

Разделы

Мы проиллюстрируем использование множественного наследования в C++, работая над небольшим примером. Предположим, что для прежнего проекта программист разработал набор классов для манипуляций со связными списками (листинг═13.1). Абстракция списка была разбита на две части: класс Link поддерживает указатели на элементы списка, а класс LinkedList запоминает начало списка. Основной смысл связного списка═≈ добавление новых элементов. Связные списки предоставляют также возможность выполнить некоторую функцию над каждым своим элементом. Функция передается в качестве аргумента. Оба эти действия поддерживаются процедурами в классе Link.

Листинг═13.1.
Классы реализации связных списков
class LinkedList
{
public:

    Link *elements;
    LinkedList()
     {
      elements = (Link *)═0;
     }

    void add(Link *n)
     {
      if (elements) elements->add(n);
      else elements = n;
     }

    void onEachDo(void f(Link *))
     {
      if (elements) elements->onEachDo(f);
     }
};

class Link
{
public:

    Link *next;
    Link()
     {
      next = (Link *)═0;
     }

    void setLink(Link *n)
     {
      next = n;
     }

    void add(Link *n)
     {
      if (next) next->add(n);
      else setLink(n);
     }

    void onEachDo(void f(Link *))
     {
      f(this);
      if(next) next->onEachDo(f);
     }
};

Мы образуем специализированные списки через определение подклассов Link. Например, класс IntegerLink в листинге═13.2 служит для поддержки списков целых чисел. Листинг 13.2 содержит также короткую программу, которая показывает, как используется эта абстракция данных.

Теперь предположим, что для нового проекта тот же самый программист должен разработать класс Tree (древовидная структура). После некоторого размышления он обнаруживает, что дерево можно представить себе как совокупность связных списков. На каждом уровне дерева поля связи указывают на подветви (деревья, принадлежащие к одному уровню). Однако каждый узел указывает также на связный список, который представляет собой его потомков. Рисунок═13.7 иллюстрирует эту структуру. Здесь наклонные стрелки обозначают указатели на потомков, а горизонтальные═≈ соединения подветвей.

Тем самым узел дерева относится и к классу LinkedList (поскольку он содержит указатель на список своих потомков), и к классу Link (поскольку он содержит указатель на свою подветвь). В языке C++ мы обозначаем множественное наследование, просто перечисляя имена надклассов, разделяя их запятыми (перечисление следует после двоеточия сразу за именем класса при его описании). Как и в случае одиночного наследования, каждому классу должно предшествовать ключевое слово (public или private), которое определяет правило видимости. В листинге═13.3 показан класс Tree, который наследует от классов

Листинг═13.2.
Уточнение класса Link
class IntegerLink : public Link
{
  int value;

public:

    IntegerLink(int i) : Link()
     {
      value = i;
     }

    print()
     {
      printf("%d\n", value);
     }
};

void display(IntegerLink *x)
{
  x->print();
}

main()
{
  LinkedList list;

  list.add(new IntegerLink(3));
  list.add(new IntegerLink(17));
  list.add(new IntegerLink(32));
  list.onEachDo(display);
}


Рис.═13.7. Дерево как совокупность связных списков

Листинг═13.3.
Пример множественного наследования
class Tree : public Link; public LinkedList
{
  int value;

public:

    Tree(int i)
     {
      value = i;
     }

    print()
     {
      printf("%d\n",value);
     }

    void add(Tree *n)
     {
      LinkedList::add(n);
     }

    void addChild(Tree *n)
     {
      Linkedlist::add(n);
     }

    void addSubling(Tree *n)
     {
      Link::add(n);
     }

    void onEachDo(void f(Link *))
     {
      /* сначала обработать потомка */
      if (elements) elements->onEachDo(f);

      /* затем себя */
      f(this);

      /* потом перейти к подветвям */
      if (next) next->onEachDo(f); 
     }
};

main()
{
  Tree *t = new Tree(17);

  t->add(new Tree(12));
  t->addSubling(new Tree(25));
  t->addChild(new Tree(15));
  t->addEachDo(display);
}

Link и LinkedList с ключевым словом public. Узлы дерева Tree содержат указатели на потомков, а также целочисленные значения.

Теперь необходимо справиться с проблемой неоднозначности. Прежде всего имеется двусмысленность в имени add (добавить), которая отражает двойственность приписываемого ему значения. Для дерева есть два смысла операции ╚добавить╩: присоединить узел-потомок и породить узел-подветвь. Первое обеспечивается функцией add класса LinkedList, второе═≈ функцией add класса Link. После некоторого размышления программист решает оставить функцию add в смысле ╚добавить узел-потомок╩, но одновременно вводит две новые функции, имена которых отражают цель операции (добавить подветвь и добавить потомка).

Заметьте, что все три функции по существу═≈ просто переименованные старые. Они не добавляют нового функционирования, а просто передают управление ранее определенным функциям. Некоторые объектно-ориентированные языки (например, Eiffel) позволяют пользователю вводить подобное переименование без создания новой функции.

Двусмысленность в методе onEachDo является более сложной. Здесь правильное действие состоит в выполнении сквозного прохода по всем узлам дерева. Процесс начинается с просмотра узлов-потомков, затем возвращается в исходный узел, а затем переходит к подветвям (которые, естественно, осуществляют рекурсивный проход уже по своим потомкам). То есть действие является комбинацией методов базовых классов Link и LinkedList, как это показано в листинге═13.4.

Переименование время от времени оказывается необходимым из-за пересечения понятий наследования и параметрической перегрузки. Когда в C++ используется перегруженное имя, то сперва вызывается механизм наследования для поиска контекста, в котором определена функция. Затем типы параметров анализируются для снятия двусмысленности в пределах данного контекста. Предположим, что есть два класса A и B, для каждого из которых определен метод display, но у методов разные аргументы (листинг═13.4). Пользователь считает, что так как эти два метода различаются по списку параметров, дочерний класс может наследовать от двух родителей и иметь доступ к обоим методам. К сожалению, здесь наследования недостаточно. Когда пользователь вызывает метод display с целочисленным аргументом, компилятор не может принять решение, использовать ли функцию из класса A (которая соответствует типу аргумента) или же из класса B (которая встречается первой при заложенном в C++ алгоритме поиска; для ее вызова аргумент будет приведен от типа integer к типу double). К счастью, компилятор всегда предупреждает о подобных случаях. Однако предупреждение выдается в точке вызова метода, а не при описании класса.

Выход в том, чтобы переопределить оба метода для дочернего класса C, как это показано в листинге═13.4. Мы избежим конкуренции между наследованием и перегрузкой═≈ в обоих случаях поиск кончается в классе C, где для компилятора уже ясно, что параметрическая перегрузка используется намеренно.

Листинг═13.4.
Взаимодействие наследования и перегрузки
class A
{
public:

    void virtual display(int i)
     {
      printf("in A %d\n", i);
     }
};

class B
{
public:

    void virtual display(double d)
     {
      printf("in B %g\n", d);
     }
};

class C: public B, public A
{
public:

    void virtual display(int i)
     {
      A::display(i);
     }

    void virtual display(double d)
     {
      B::display(d);
     }
};

main()
{
  C c;

  c.display(13);
  c.display(3.14);
}

В предыдущем разделе мы описали трудность, которая возникает, когда класс наследует от двух родителей, имеющих общего предка. Эта проблема была проиллюстрирована на примере классов InStream и OutStream, каждый из которых наследовал от общего класса Stream. Если мы хотим, чтобы порождаемый класс наследовал только одну копию полей данных, определенных в классе Stream, то промежуточные классы InStream и OutStream должны определять, что их наследование от общего родительского класса является виртуальным. Ключевое слово virtual показывает, что надкласс может появляться более одного раза в подклассах, порождаемых из определяемого класса, но при этом нужно оставлять только одну его копию. Листинг═13.5 показывает такой вариант описания для этих четырех классов.

Листинг═13.5.
Пример виртуального наследования
class Stream
{
  File *fid;
  ...
};

class InStream : public virtual Stream
{
  ...
  int open(File *);
};

class OutStream : public virtual Stream
{
  ...
  int open(File *);
};

class InOutStream: public InStream, public OutStream
{
  ...
};

Такой подход, использованный в языке C++, нельзя признать совершенным (как на это указывает Мейер), поскольку конфликт имен возникает на уровне дочерних классов, а решение (ключевое слово virtual для общего предка) затрагивает родительские классы. То есть ╚виртуальное предназначение╩ общей части закладывается в родителях, а не в комбинированном классе.

В редких случаях желательно создавать две копии наследуемых полей данных. Например, и графические объекты, и колоды игральных карт могут быть основаны на связных списках, поэтому оба класса выводятся из класса Link. Поскольку эти два списка являются независимыми, они оба должны содержаться в комбинированном классе GraphicalDeck. В такой ситуации ключевые слова virtual опускаются═≈ желаемый результат будет достигнут. Однако важно гарантировать, что возникший конфликт имен не вызовет ошибочной интерпретации.

Ключевые слова, определяющие видимость, имеют право отличаться в разных родительских классах. Следовательно, виртуальный наследник может быть порожден с различными атрибутами, например public и protected. В таком случае наиболее жесткий уровень защиты (в нашем примере protected) игнорируется и используется менее ограничительная категория.

Когда конструкторы определены в нескольких надклассах, важен порядок выполнения родительских конструкторов и, следовательно, очередность инициализации полей данных. Пользователь может управлять этим, вызывая непосредственно конструкторы родительских классов внутри конструктора потомков. Например, в листинге═13.6 пользователь явно указывает, что при инициализации класса C конструктор класса B вызывается первым, то есть до вызова конструктора класса A. Порядок вызова конструкторов влияет на инициализацию.

Исключение из этого правила составляют виртуальные базовые классы. Они всегда инициализируются лишь один раз вызовом безаргументного конструктора (если обращение к нему не осуществляется пользователем, такой конструктор вызывается системой). Это происходит до какой бы то ни было другой инициализации. В листинге═13.6 инициализация при создании нового элемента класса C будет производиться в таком порядке: инициализируется класс D с помощью безаргументного конструктора, затем═≈ класс B и наконец═≈ класс A. Два кажущихся вызова конструктора класса D внутри конструкторов классов A и B не имеют никакого эффекта, поскольку указано, что родительский класс виртуален.

Если требуется, чтобы для конструктора базового класса задавались аргументы, класс C может законным образом задать нужные значения даже тогда, когда D не

Листинг═13.6.
Конструкторы при множественном наследовании
class D
{
public:

    D(){  ...  }
    D(int i){  ...  }
    D(double d){  ...  }
};

class A : virtual D
{
public:

    A() : D(7)
     {  ...  }
};

class B : virtual D
{
public:

    B() : D(3.14)
     {  ...  }
};

class C : public A, public B
{
public:

    C() : B(), A()
     {  ...  }
};

является непосредственным предком класса C. Это единственная ситуация, когда внутри класса разрешено использовать конструктор другого класса, который не является непосредственно предшествующим родителем. То есть конструктор класса C может быть записан следующим образом:

C() : D(12), B(), A() { ... }

Конструкторы для виртуальных базовых классов должны вызываться первыми, то есть до конструкторов невиртуальных предков.

Виртуальные методы, определенные в виртуальных базовых классов, также могут быть источником проблем. Предположим, что каждый из четырех классов в листинге═13.5 обладает методом с именем initialize(). Он определен как виртуальный в классе Stream и переопределяется в каждом из последующих трех классов. Методы initialize() в классах InStream и OutStream вызывают Stream::initialize() и, кроме того, выполняют некоторую специфическую для каждого из классов инициализацию.

Теперь рассмотрим метод initialize() для класса InOutStream. Он не может вызвать оба унаследованных метода InStream::initialize() и OutStream::initialize() без того, чтобы не вызвать дважды метод Stream::initialize(). Повторное обращение к методу Stream::initialize(), вероятно, будет иметь побочные эффекты. Способ из-бежать этой проблемы: переписать Stream::initialize() так, чтобы он определял, была ли уже осуществлена инициализация. Другой вариант: переопределить методы классов InStream и OutStream, чтобы они не вызывали метод класса Stream. В последнем случае класс InOutStream должен в явном виде обращаться к процедуре инициализации каждого из трех классов.

13.5. Множественное наследование в Java

Разделы

Язык Java не поддерживает множественное наследование классов, но реализует множественное наследование интерфейсов. Класс может указать, что он поддерживает несколько различных интерфейсов. Например, один интерфейс может требовать запоминания данных на диске, а другой═≈ определять протокол самоотображения объектов. Запоминаемый графический объект будет поддерживать оба эти интерфейса:

class graphicalObject implements Storable, Graphical
{
  // ...
}

В то время как классы не могут наследовать от двух и более классов (расширяя их), интерфейсам это разрешено. Мы имеем право определить интерфейс для запоминаемых графических объектов следующим образом:

interface GraphicalObject extends Storable, Graphical
{
  // ...
}

Литература для дальнейшего чтения

Разделы

Критика множественного наследования встречается у Саккинена [Sakkinen 1988a]. Упомянутая работа является сокращенной адаптированной версией его Ph. D. диссертации [Sakkinen═1992]. Объяснение множественного наследования в языке C++ дается Эллис [Ellis═1990].

Упражнения

Разделы

  1. Приведите два примера множественного наследования в ситуациях, не связанных с компьютерами.
  2. В работе [Wiener═1989] описан ╚практический пример множественного наследования в C++╩. Определен класс IntegerArray, который наследует от двух классов Array и Integer. Как вы думаете, является ли это хорошим примером множественного наследования? Обоснуйте свой ответ.
  3. Модифицируйте определение класса Tree так, чтобы он мог быть использован как двоичное дерево. Обеспечьте средства для поиска или изменения левого или правого потомка любого узла. Какие предположения вам требуются?
  4. Обобщите вашу работу над упражнением═3 так, чтобы создать поисковое двоичное дерево. Оно содержит список целых чисел со следующим свойством: значение в каждом узле больше, чем значения в левой подветви, и меньше или равно значениям в правой подветви.
  5. Обсудите виртуальное наследование в языке C++ с точки зрения принципов Парнаса о маскировке информации.

Множественное наследование