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

StackOverflow https://stackoverflow.com/questions/225929

Вопрос

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

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

Это было полезно?

Решение

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

Допустим, есть два класса 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 #, они знают, что поддержка множественного наследования значительно усложнит реализацию компилятора, и, по-видимому, они не думают, что это преимущество стоит затрат.

Другие советы

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

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

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

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

  

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

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

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

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

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

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

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

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

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

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

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

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

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

public sealed class CustomerEditView : Form, MVCView<Customer>

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

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

Common Lisp Object System (CLOS) - это еще один пример того, что поддерживает MI, избегая при этом проблем в стиле C ++: наследованию присваивается разумное значение по умолчанию , в то же время предоставляя вам свободу явно определять, как именно, скажем, вызывать поведение супер.

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

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

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

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

Одна из целей разработки таких фреймворков, как 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).

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

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

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

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

Еще один интересный пример ( не для C ++):

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

Здесь только B использует виртуальное наследование. Таким образом, E содержит два B , которые совместно используют один и тот же A . Таким образом, вы можете получить указатель A * , который указывает на E , но вы не можете & # 8217; привести его к указателю B * хотя объект является , фактически 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;
}

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top