it-roy-ru.com

Какова точная проблема с множественным наследованием?

Я вижу людей, постоянно спрашивающих, должно ли множественное наследование быть включено в следующую версию C # или Java. Люди С ++, которым посчастливилось обладать такой способностью, говорят, что это все равно, что дать кому-то веревку, чтобы в итоге повеситься.

Что случилось с множественным наследованием? Есть ли конкретные образцы?

112
Vlad Gudim

Наиболее очевидная проблема заключается в переопределении функций.

Допустим, есть два класса A и B, оба из которых определяют метод doSomething. Теперь вы определяете третий класс C, который наследуется от A и B, но вы не переопределяете метод doSomething.

Когда компилятор запустит этот код ...

C c = new C();
c.doSomething();

... какую реализацию метода он должен использовать? Без каких-либо дополнительных разъяснений компилятор не сможет устранить неоднозначность.

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

Такие языки, как C++ и Java и ​​C #, создают фиксированный макет на основе адреса для каждого типа объекта. Что-то вроде этого:

class A:
    at offset 0 ... "abc" ... 4 byte int field
    at offset 4 ... "xyz" ... 8 byte double field
    at offset 12 ... "speak" ... 4 byte function pointer

class B:
    at offset 0 ... "foo" ... 2 byte short field
    at offset 2 ... 2 bytes of alignment padding
    at offset 4 ... "bar" ... 4 byte array pointer
    at offset 8 ... "baz" ... 4 byte function pointer

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

Многократное наследование делает его очень сложным.

Если класс C наследуется от A и B, компилятор должен решить, расположить ли данные в порядке AB или в порядке BA.

Но теперь представьте, что вы вызываете методы для объекта B. Это действительно просто B? Или это на самом деле объект C, вызываемый полиморфно через его интерфейс B? В зависимости от фактической идентичности объекта, физическое расположение будет различным, и невозможно определить смещение функции, вызываемой на сайте вызова.

Способ справиться с этим типом системы состоит в том, чтобы отказаться от подхода с фиксированной разметкой, позволяя каждому объекту запрашивать его разметку до попытки вызова функций или доступа к их полям.

Итак ... Короче говоря ... это боль в шее для авторов компилятора для поддержки множественного наследования. Поэтому, когда кто-то, как Гвидо ван Россум, разрабатывает python, или когда Андерс Хейлсберг разрабатывает c #, они знают, что поддержка множественного наследования значительно усложнит реализацию компилятора, и, по-видимому, они не думают, что это преимущество стоит затрат.

77
benjismith

Проблемы, о которых вы, ребята, упоминаете, не так уж сложно решить. Например, Эйфелева делает это на отлично! (и без введения произвольного выбора или чего-либо еще)

Например. если вы наследуете от A и B, оба имеют метод foo (), то, конечно, вам не нужен произвольный выбор в вашем классе C, наследующий от A и B. Вы должны либо переопределить foo, чтобы было ясно, что будет использоваться если вызывается c.foo () или иначе вы должны переименовать один из методов в C. (он может стать bar ())

Также я думаю, что множественное наследование часто весьма полезно. Если вы посмотрите на библиотеки Eiffel, вы увидите, что они используются повсеместно, и лично я пропустил эту функцию, когда мне пришлось вернуться к программированию на Java.

44
Daniel

проблема с бриллиантами :

неоднозначность, которая возникает, когда два класса B и C наследуются от A, а класс D наследуется от B и C. Если есть метод в A, который B и C имеет переопределено , а D не переопределяет Итак, какую версию метода наследует D: версию B или версию C?

... Это называется "проблема алмазов" из-за формы диаграммы наследования классов в этой ситуации. В этом случае класс A находится сверху, а B и C - отдельно под ним, а D соединяет их вместе внизу, образуя ромбовидную форму ...

26
J Francis

Множественное наследование - это одна из тех вещей, которая используется не часто и может использоваться неправильно, но иногда необходима.

Я никогда не понимал, не добавляя функцию, просто потому, что она может быть использована неправильно, когда нет хороших альтернатив. Интерфейсы не являются альтернативой множественному наследованию. С одной стороны, они не позволяют вам применять предварительные условия или постусловия. Как и любой другой инструмент, вам нужно знать, когда его использовать и как его использовать.

21
KeithB

скажем, у вас есть объекты A и B, которые оба наследуются C. A и B оба реализуют foo (), а C - нет. Я звоню C.foo (). Какая реализация выбрана? Есть и другие проблемы, но этот тип вещей большой.

16
tloach

Я не думаю, что проблема с алмазами - это проблема, я бы рассмотрел эту софистику и ничего больше.

Худшая проблема, с моей точки зрения, с множественным наследованием - это RAD - жертвы и люди, которые утверждают, что являются разработчиками, но на самом деле застряли с половинным знанием (в лучшем случае).

Лично я был бы очень рад, если бы мог наконец-то сделать что-то в Windows Forms, как это (это не правильный код, но он должен дать вам идею):

public sealed class CustomerEditView : Form, MVCView<Customer>

Это основная проблема, которую я имею, не имея множественного наследования. Вы МОЖЕТЕ сделать что-то подобное с интерфейсами, но есть то, что я называю "кодом ***", именно этот болезненный повторяющийся код *** вы, например, должны писать в каждом из ваших классов, чтобы получить контекст данных.

По моему мнению, не должно быть абсолютно ни малейшей необходимости в ЛЮБОМ повторении кода на современном языке.

5
Turing Complete

Основная проблема с множественным наследованием хорошо подытожена на примере tloach. При наследовании от нескольких базовых классов, которые реализуют одну и ту же функцию или поле, компилятор должен принять решение о том, какую реализацию наследовать.

Это становится хуже, когда вы наследуете от нескольких классов, которые наследуют от одного базового класса. (наследование алмазов, если вы рисуете дерево наследования, вы получаете форму ромба)

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

Я считаю, что при выполнении хорошего OO дизайна мне никогда не нужно множественное наследование. В тех случаях, когда это мне нужно, я обычно нахожу, что использую наследование для повторного использования функциональности, тогда как наследование подходит только для отношений "is-a".

Существуют и другие методы, такие как миксины, которые решают те же проблемы и не имеют проблем, связанных с множественным наследованием.

5
Mendelt

Общая объектная система LISP (CLOS) - это еще один пример чего-то, что поддерживает MI, избегая при этом проблем в стиле C++: наследованию присваивается разумное значение по умолчанию , в то же время предоставляя вам свободу явного решения, как именно скажем, назвать поведение супер.

3
Frank Shearar

Одна из целей разработки таких фреймворков, как Java и ​​.NET, - сделать так, чтобы код, скомпилированный для работы с одной версией предварительно скомпилированной библиотеки, одинаково хорошо работал с последующими версиями этой библиотеки. , даже если эти последующие версии добавляют новые функции. В то время как обычная парадигма в таких языках, как C или C++, заключается в распространении статически связанных исполняемых файлов, которые содержат все необходимые им библиотеки, парадигма в .NET и Java заключается в распространении приложений в виде наборов компонентов, которые " связаны "во время выполнения.

Модель COM, которая предшествовала .NET, пыталась использовать этот общий подход, но на самом деле у нее не было наследования - вместо этого каждое определение класса эффективно определяло как класс, так и интерфейс с тем же именем, которое содержало все его открытые члены. Экземпляры были типа класса, в то время как ссылки были типа интерфейса. Объявленный класс как производный от другого был эквивалентен объявлению класса как реализующего интерфейс другого, и требовал, чтобы новый класс повторно реализовал все открытые члены классов, из которых один произошел. Если Y и Z являются производными от X, а затем W происходит от Y и Z, не имеет значения, будут ли Y и Z реализовывать элементы X по-разному, потому что Z не сможет использовать их реализации - ему придется определить его своя. W мог бы инкапсулировать экземпляры Y и/или Z и связывать свои реализации методов X через их, но не было бы никакой двусмысленности относительно того, что должны делать методы X - они делали бы то, что код Z явно указывал им делать.

Сложность в Java и ​​.NET заключается в том, что коду разрешено наследовать элементы и иметь к ним доступ неявно ссылаться на родительские элементы. Предположим, что у одного были классы W-Z, связанные как выше:

class X { public virtual void Foo() { Console.WriteLine("XFoo"); }
class Y : X {};
class Z : X {};
class W : Y, Z  // Not actually permitted in C#
{
  public static void Test()
  {
    var it = new W();
    it.Foo();
  }
}

Может показаться, что W.Test() должен создать экземпляр W для вызова виртуального метода Foo, определенного в X. Предположим, однако, что Y и Z на самом деле были в отдельно скомпилированном модуле, и хотя они были определены, как указано выше, когда X и W были скомпилированы, они позже были изменены и перекомпилированы:

class Y : X { public override void Foo() { Console.WriteLine("YFoo"); }
class Z : X { public override void Foo() { Console.WriteLine("ZFoo"); }

Теперь, каков должен быть эффект вызова W.Test()? Если бы перед распространением программу нужно было статически связать, то на этапе статического связывания можно было бы заметить, что, хотя программа не имела неоднозначности до того, как Y и Z были изменены, изменения в Y и Z сделали вещи двусмысленными, и компоновщик мог отказаться от постройте программу, если или пока не решена такая неоднозначность. С другой стороны, вполне возможно, что человек, у которого есть и W, и новые версии Y и Z, - это тот, кто просто хочет запустить программу и не имеет исходного кода ни для одной из них. Когда запускается W.Test(), больше не будет понятно, что должен делать W.Test(), но пока пользователь не попытается запустить W с новой версией Y и Z, ни одна часть системы не сможет распознать проблему (если только W был признан незаконным даже до внесения изменений в Y и Z).

2
supercat

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

Язык Eiffel поддерживает множественное наследование без ограничений очень эффективным и продуктивным способом, но с самого начала этот язык был разработан для его поддержки.

Эта функция сложна для реализации для разработчиков компиляторов, но кажется, что этот недостаток может быть компенсирован тем фактом, что хорошая поддержка множественного наследования может избежать поддержки других функций (то есть не требуется интерфейс или метод расширения).

Я думаю, что поддержка множественного наследования или нет - это больше вопрос выбора, вопрос приоритетов. Более сложная функция требует больше времени для правильной реализации и работы и может быть более противоречивой. Реализация C++ может быть причиной, по которой множественное наследование не было реализовано в C # и Java ...

2
Christian Lemer

Алмаз не является проблемой, если вы не используете что-то вроде виртуального наследования C++: при обычном наследовании каждый базовый класс напоминает поле члена (на самом деле они расположены в RAM таким образом), давая вам некоторый синтаксический сахар и дополнительную возможность переопределять больше виртуальных методов. Это может наложить некоторую двусмысленность во время компиляции, но это обычно легко решить.

С другой стороны, с виртуальным наследованием он слишком легко выходит из-под контроля (а затем становится беспорядком). Рассмотрим в качестве примера диаграмму "сердце":

  A       A
 / \     / \
B   C   D   E
 \ /     \ /
  F       G
    \   /
      H

В C++ это совершенно невозможно: как только F и G объединяются в один класс, их As также объединяются, точка. Это означает, что вы никогда не можете считать базовые классы непрозрачными в C++ (в этом примере вы должны создать A в H, поэтому вы должны знать, что он присутствует где-то в иерархии). На других языках это может работать, однако; например, F и G могли бы явно объявить A как "внутренний", тем самым запретив последующее слияние и фактически сделав себя твердыми.

Еще один интересный пример (не C++ - специфичный):

  A
 / \
B   B
|   |
C   D
 \ /
  E

Здесь только B использует виртуальное наследование. Таким образом, E содержит два Bs, которые имеют одно и то же A. Таким образом, вы можете получить указатель A*, который указывает на E, но вы не можете привести его к указателю B*, хотя объект is фактически B, поскольку такое приведение является неоднозначным, и эта неоднозначность не может быть обнаруженным во время компиляции (если компилятор не видит всю программу). Вот тестовый код:

struct A { virtual ~A() {} /* so that the class is polymorphic */ };
struct B: virtual A {};
struct C: B {};
struct D: B {};
struct E: C, D {};

int main() {
        E data;
        E *e = &data;
        A *a = dynamic_cast<A *>(e); // works, A is unambiguous
//      B *b = dynamic_cast<B *>(e); // doesn't compile
        B *b = dynamic_cast<B *>(a); // NULL: B is ambiguous
        std::cout << "E: " << e << std::endl;
        std::cout << "A: " << a << std::endl;
        std::cout << "B: " << b << std::endl;
// the next casts work
        std::cout << "A::C::B: " << dynamic_cast<B *>(dynamic_cast<C *>(e)) << std::endl;
        std::cout << "A::D::B: " << dynamic_cast<B *>(dynamic_cast<D *>(e)) << std::endl;
        std::cout << "A=>C=>B: " << dynamic_cast<B *>(dynamic_cast<C *>(a)) << std::endl;
        std::cout << "A=>D=>B: " << dynamic_cast<B *>(dynamic_cast<D *>(a)) << std::endl;
        return 0;
}

Более того, реализация может быть очень сложной (зависит от языка; см. Ответ Бенджисмита).

2
number Zero