Я вижу, как люди все время спрашивают, следует ли включать множественное наследование в следующую версию C # или Java. Люди C ++, которым посчастливилось обладать этой способностью, говорят, что это все равно, что дать кому-то веревку, чтобы в конце концов повеситься.
Что с множественным наследованием? Есть конкретные образцы?
oop
multiple-inheritance
language-theory
Влад Гудим
источник
источник
Ответы:
Самая очевидная проблема связана с переопределением функции.
Скажем, есть два класса
A
иB
, каждый из которых определяет методdoSomething
. Теперь можно определить третий классC
, который наследует от обоихA
иB
, но вы не переопределитьdoSomething
метод.Когда компилятор заполняет этот код ...
... какую реализацию метода следует использовать? Без дополнительных разъяснений компилятор не может разрешить двусмысленность.
Помимо переопределения, другой большой проблемой множественного наследования является расположение физических объектов в памяти.
Такие языки, как C ++, Java и C #, создают фиксированный адресный макет для каждого типа объекта. Что-то вроде этого:
Когда компилятор генерирует машинный код (или байт-код), он использует эти числовые смещения для доступа к каждому методу или полю.
Множественное наследование очень усложняет задачу.
Если класс
C
наследуется от обоихA
иB
, компилятор должен решить, размещать ли данные поAB
порядку или поBA
порядку.Но теперь представьте, что вы вызываете методы
B
объекта. Неужели это простоB
? Или это действительноC
объект, вызываемый полиморфно через егоB
интерфейс? В зависимости от фактической идентичности объекта физическая структура будет отличаться, и невозможно узнать смещение функции, вызываемой на сайте вызова.Чтобы справиться с такой системой, нужно отказаться от подхода с фиксированным макетом, позволяя каждому объекту запрашивать его макет перед попыткой вызвать функции или получить доступ к его полям.
Итак ... короче ... авторам компиляторов сложно поддерживать множественное наследование. Поэтому, когда кто-то вроде Гвидо ван Россума разрабатывает python или Андерс Хейлсберг разрабатывает C #, они знают, что поддержка множественного наследования значительно усложняет реализации компилятора, и, по-видимому, они не думают, что выгода стоит затрат.
источник
Проблемы, о которых вы упомянули, на самом деле не так уж и сложно решить. На самом деле, например, Эйфель прекрасно это делает! (и без введения произвольного выбора или чего-то еще)
Например, если вы наследуете от A и B, оба из которых имеют метод foo (), тогда, конечно, вам не нужен произвольный выбор в вашем классе C, унаследованный от обоих A и B. Вам нужно либо переопределить foo, чтобы было ясно, что будет используется, если вызывается c.foo () или в противном случае вам нужно переименовать один из методов в C. (он может стать bar ())
Также я считаю, что множественное наследование часто бывает весьма полезным. Если вы посмотрите библиотеки Eiffel, вы увидите, что он используется повсеместно, и лично я пропустил эту функцию, когда мне пришлось вернуться к программированию на Java.
источник
Проблема с бриллиантами :
источник
someZ
и он хочет привести его к,Object
а затем кB
? ЧтоB
он получит?Object
и обратно ...Множественное наследование - одна из тех вещей, которые используются не часто и могут использоваться неправильно, но иногда они необходимы.
Я никогда не понимал, что не добавляю функцию только потому, что ее можно использовать неправильно, когда нет хороших альтернатив. Интерфейсы не являются альтернативой множественному наследованию. Во-первых, они не позволяют вам применять предварительные условия или постусловия. Как и любой другой инструмент, вам нужно знать, когда он уместен и как его использовать.
источник
assert
?допустим, у вас есть объекты A и B, которые наследуются C. A и B оба реализуют foo (), а C - нет. Я вызываю C.foo (). Какая реализация будет выбрана? Есть и другие проблемы, но такие вещи очень серьезные.
источник
Основная проблема множественного наследования хорошо резюмируется на примере tloach. При наследовании от нескольких базовых классов, реализующих одну и ту же функцию или поле, компилятор должен принять решение о том, какую реализацию наследовать.
Это становится еще хуже, когда вы наследуете от нескольких классов, которые наследуются от одного и того же базового класса. (алмазное наследование, если вы нарисуете дерево наследования, вы получите форму ромба)
Для компилятора нетрудно решить эти проблемы. Но выбор, который должен сделать здесь компилятор, довольно произвольный, что делает код менее интуитивным.
Я считаю, что при хорошем объектно-ориентированном дизайне мне никогда не понадобится множественное наследование. В тех случаях, когда мне это действительно нужно, я обычно нахожу, что использую наследование для повторного использования функциональных возможностей, в то время как наследование подходит только для отношений «is-a».
Существуют и другие методы, такие как миксины, которые решают те же проблемы и не имеют проблем, присущих множественному наследованию.
источник
([..bool..]? "test": 1)
?Я не думаю, что проблема с бриллиантами - это проблема, я считаю эту софизму, и ничего больше.
Наихудшая проблема, с моей точки зрения, с множественным наследованием - это жертвы RAD и люди, которые утверждают, что они разработчики, но на самом деле застряли на полузнании (в лучшем случае).
Лично я был бы очень рад, если бы наконец смог сделать что-то подобное в Windows Forms (это неправильный код, но он должен дать вам идею):
Это основная проблема, с которой я столкнулся с отсутствием множественного наследования. Вы МОЖЕТЕ сделать что-то подобное с интерфейсами, но есть то, что я называю «дерьмовым кодом», это мучительный повторяющийся треп, который вы должны написать в каждом из ваших классов, например, чтобы получить контекст данных.
На мой взгляд, в ЛЮБОМ повторении кода на современном языке не должно быть абсолютно никакой необходимости, ни в малейшей степени.
источник
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,
Сложность Java и .NET заключается в том, что коду разрешено наследовать члены и иметь доступ к ним, неявно ссылаясь на родительские члены. Предположим, у кого-то есть классы WZ, связанные, как указано выше:
Казалось бы,
W.Test()
создание экземпляра W вызовет реализацию виртуального метода,Foo
определенного вX
. Однако предположим, что Y и Z на самом деле были в отдельно скомпилированном модуле, и хотя они были определены, как указано выше, при компиляции X и W, позже они были изменены и перекомпилированы:Каким должен быть эффект от звонка
W.Test()
? Если бы программа должна была быть статически связана перед распространением, на этапе статической ссылки можно было бы определить, что, хотя программа не имела двусмысленности до изменения Y и Z, изменения Y и Z сделали вещи неоднозначными, и компоновщик мог отказаться строить программу, пока такая неоднозначность не будет разрешена. С другой стороны, возможно, что человек, у которого есть как W, так и новые версии Y и Z, просто хочет запустить программу и не имеет исходного кода ни для одной из них. ПриW.Test()
запуске уже не будет понятно, чтоW.Test()
должен работать, но до тех пор, пока пользователь не попытается запустить W с новой версией Y и Z, никакая часть системы не сможет распознать наличие проблемы (если W не считался незаконным даже до изменений в Y и Z) ,источник
Алмаз не является проблемой, пока вы не используете что-либо вроде виртуального наследования C ++: при обычном наследовании каждый базовый класс напоминает поле-член (на самом деле они размещены в ОЗУ таким образом), что дает вам некоторый синтаксический сахар и дополнительная возможность переопределить больше виртуальных методов. Это может вызвать некоторую двусмысленность во время компиляции, но обычно это легко решить.
С другой стороны, с виртуальным наследованием оно слишком легко выходит из-под контроля (а затем превращается в беспорядок). Рассмотрим в качестве примера диаграмму «сердце»:
В C ++ это совершенно невозможно: как только
F
иG
объединяются в один класс,A
объединяются и их s, точка. Это означает , что вы никогда не рассматривать базовые классы непрозрачных в C ++ (в этом примере вы должны построитьA
вH
так что вы должны знать , что она присутствует где - то в иерархии). Однако на других языках это может работать; например,F
иG
может явно объявить A как «внутреннее», тем самым запретив последующее слияние и эффективно сделав себя прочным.Еще один интересный пример ( не специфичный для C ++):
Здесь
B
используется только виртуальное наследование. SoE
содержит дваB
одинаковых sA
. Таким образом, вы можете получитьA*
указатель , который указывает наE
, но вы не можете бросить его кB*
указателю , хотя объект является на самом делеB
как таковой бросок неоднозначна, и эта неопределенность не может быть обнаружена во время компиляции (если компилятор не видит вся программа). Вот тестовый код:Более того, реализация может быть очень сложной (зависит от языка; см. Ответ Бенджисмита).
источник