Eu posso ver pessoas perguntando o tempo todo se a herança múltipla deve ser incluída na próxima versão do C # ou Java. As pessoas de C ++, que têm a sorte de ter essa capacidade, dizem que isso é como dar a alguém uma corda para se enforcar.
Qual é o problema da herança múltipla? Existem amostras de concreto?
oop
multiple-inheritance
language-theory
Vlad Gudim
fonte
fonte
Respostas:
O problema mais óbvio é a substituição da função.
Digamos que tenha duas classes
A
eB
, ambas definam um métododoSomething
. Agora você define uma terceira classeC
, que herda de ambosA
eB
, mas não substitui odoSomething
método.Quando o compilador propaga esse código ...
... qual implementação do método ele deve usar? Sem mais esclarecimentos, é impossível para o compilador resolver a ambiguidade.
Além de substituir, o outro grande problema com herança múltipla é o layout dos objetos físicos na memória.
Idiomas como C ++, Java e C # criam um layout fixo baseado em endereço para cada tipo de objeto. Algo assim:
Quando o compilador gera código de máquina (ou código de código), ele usa esses deslocamentos numéricos para acessar cada método ou campo.
A herança múltipla torna muito complicado.
Se a classe
C
herda de ambosA
eB
, o compilador deve decidir se deseja layout dos dados emAB
ordem ou emBA
ordem.Mas agora imagine que você está chamando métodos em um
B
objeto. É realmente apenas umB
? Ou é realmente umC
objeto chamado polimorficamente, por meio de suaB
interface? Dependendo da identidade real do objeto, o layout físico será diferente e é impossível saber o deslocamento da função a ser chamada no local da chamada.A maneira de lidar com esse tipo de sistema é abandonar a abordagem de layout fixo, permitindo que cada objeto seja consultado por seu layout antes de tentar invocar as funções ou acessar seus campos.
Então ... para encurtar a história ... é uma dor de cabeça para os autores de compiladores apoiarem herança múltipla. Então, quando alguém como Guido van Rossum cria python, ou quando Anders Hejlsberg cria c #, eles sabem que o suporte à herança múltipla tornará as implementações do compilador significativamente mais complexas e, presumivelmente, elas não acham que o benefício vale o custo.
fonte
Os problemas que vocês mencionam não são tão difíceis de resolver. De fato, por exemplo, Eiffel faz isso perfeitamente bem! (e sem introduzir escolhas arbitrárias ou o que seja)
Por exemplo, se você herda de A e B, ambos com o método foo (), é claro que não deseja uma escolha arbitrária em sua classe C herdando ambos A e B. Você deve redefinir foo para que fique claro o que será usado se c.foo () for chamado ou você deve renomear um dos métodos em C. (pode se tornar bar ())
Também acho que a herança múltipla geralmente é bastante útil. Se você olhar para as bibliotecas de Eiffel, verá que ele é usado em todo o lugar e, pessoalmente, eu perdi o recurso quando tive que voltar à programação em Java.
fonte
O problema do diamante :
fonte
someZ
e quer lançá-lo paraObject
e depoisB
? QualB
será?Object
e de volta para esse tipo ...A herança múltipla é uma daquelas coisas que não é usada com freqüência e pode ser mal utilizada, mas às vezes é necessária.
Eu nunca entendi não adicionar um recurso, apenas porque ele pode ser mal utilizado, quando não há boas alternativas. As interfaces não são uma alternativa à herança múltipla. Por um lado, eles não permitem impor pré-condições ou pós-condições. Assim como qualquer outra ferramenta, você precisa saber quando é apropriado usá-lo e como usá-lo.
fonte
assert
?digamos que você tenha objetos A e B que são herdados por C. A e B implementam foo () e C não. Eu chamo C.foo (). Qual implementação é escolhida? Existem outras questões, mas esse tipo de coisa é grande.
fonte
O principal problema da herança múltipla está bem resumido no exemplo da abordagem. Ao herdar de várias classes base que implementam a mesma função ou campo, o compilador precisa tomar uma decisão sobre qual implementação herdar.
Isso fica pior quando você herda de várias classes que herdam da mesma classe base. (herança de diamante, se você desenhar a árvore de herança, obtém uma forma de diamante)
Esses problemas não são realmente problemáticos para um compilador superar. Mas a escolha que o compilador deve fazer aqui é bastante arbitrária, isso torna o código muito menos intuitivo.
Acho que, ao fazer um bom projeto de OO, nunca preciso de herança múltipla. Nos casos em que preciso, geralmente descubro que tenho usado a herança para reutilizar a funcionalidade, enquanto a herança é apropriada apenas para relações "é-a".
Existem outras técnicas, como mixins, que resolvem os mesmos problemas e não têm os problemas que a herança múltipla possui.
fonte
([..bool..]? "test": 1)
?Eu não acho que o problema do diamante seja um problema, eu consideraria esse sofisma, nada mais.
O pior problema, do meu ponto de vista, com herança múltipla é a RAD - vítimas e pessoas que afirmam ser desenvolvedores, mas na realidade estão presas a meio conhecimento (na melhor das hipóteses).
Pessoalmente, eu ficaria muito feliz se finalmente pudesse fazer algo no Windows Forms como este (não é o código correto, mas deve lhe dar a idéia):
Este é o principal problema que tenho em não ter herança múltipla. Você PODE fazer algo semelhante com interfaces, mas existe o que eu chamo de "código ***", é esse doloroso e repetitivo *** que você precisa escrever em cada uma de suas classes para obter um contexto de dados, por exemplo.
Na minha opinião, não deve haver absolutamente nenhuma necessidade, nem um pouco, de QUALQUER repetição de código em uma linguagem moderna.
fonte
O Common Lisp Object System (CLOS) é outro exemplo de algo que suporta MI, evitando os problemas do estilo C ++: a herança recebe um padrão sensato , enquanto ainda permite a liberdade de decidir explicitamente como exatamente, digamos, chamar o comportamento de um super .
fonte
Não há nada errado na herança múltipla em si. O problema é adicionar herança múltipla a um idioma que não foi projetado com herança múltipla em mente desde o início.
A língua Eiffel está suportando herança múltipla sem restrições de uma maneira muito eficiente e produtiva, mas a linguagem foi projetada desde o início para suportá-la.
Esse recurso é complexo de implementar para desenvolvedores de compiladores, mas parece que essa desvantagem pode ser compensada pelo fato de que um bom suporte de herança múltipla pode evitar o suporte de outros recursos (ou seja, não há necessidade de Interface ou Método de Extensão).
Eu acho que apoiar a herança múltipla ou não é mais uma questão de escolha, uma questão de prioridades. Um recurso mais complexo leva mais tempo para ser corretamente implementado e operacional e pode ser mais controverso. A implementação C ++ pode ser a razão pela qual a herança múltipla não foi implementada em C # e Java ...
fonte
Um dos objetivos de design de estruturas como Java e .NET é possibilitar que o código compilado funcione com uma versão de uma biblioteca pré-compilada, que funcione igualmente bem com as versões subseqüentes dessa biblioteca, mesmo que essas versões subseqüentes adicione novos recursos. Enquanto o paradigma normal em linguagens como C ou C ++ é distribuir executáveis vinculados estaticamente que contêm todas as bibliotecas de que precisam, o paradigma no .NET e Java é distribuir aplicativos como coleções de componentes "vinculados" no tempo de execução .
O modelo COM que precedeu o .NET tentou usar essa abordagem geral, mas na verdade não tinha herança - em vez disso, cada definição de classe definiu efetivamente uma classe e uma interface com o mesmo nome que continha todos os seus membros públicos. As instâncias eram do tipo classe, enquanto as referências eram do tipo interface. Declarar uma classe como derivada de outra era equivalente a declarar uma classe como implementando a interface da outra e exigia que a nova classe reimplemente todos os membros públicos das classes das quais uma derivou. Se Y e Z derivam de X e W deriva de Y e Z, não importa se Y e Z implementam os membros de X de maneira diferente, porque Z não poderá usar suas implementações - terá que definir sua próprio. W pode encapsular instâncias de Y e / ou Z,
A dificuldade em Java e .NET é que o código tem permissão para herdar membros e ter acesso a eles referindo-se implicitamente aos membros pais. Suponha que alguém tenha classes WZ relacionadas como acima:
Parece
W.Test()
que a criação de uma instância de W chama a implementação do método virtualFoo
definido emX
. Suponha, no entanto, que Y e Z estejam realmente em um módulo compilado separadamente e, embora tenham sido definidos como acima quando X e W foram compilados, posteriormente foram alterados e recompilados:Agora, qual deve ser o efeito de ligar
W.Test()
? Se o programa tiver que ser vinculado estaticamente antes da distribuição, o estágio de link estático poderá discernir que, embora o programa não tenha ambiguidade antes que Y e Z sejam alterados, as alterações em Y e Z tornaram as coisas ambíguas e o vinculador pode se recusar a crie o programa, a menos ou até que essa ambiguidade seja resolvida. Por outro lado, é possível que a pessoa que possui W e as novas versões de Y e Z seja alguém que simplesmente queira executar o programa e não possua nenhum código-fonte. QuandoW.Test()
executado, não fica mais claro o queW.Test()
deveria funcionar, mas até que o usuário tentasse executar W com a nova versão de Y e Z, nenhuma parte do sistema reconheceria que havia um problema (a menos que W fosse considerado ilegítimo antes mesmo das alterações em Y e Z) .fonte
O diamante não é um problema, desde que você não use nada como herança virtual C ++: na herança normal, cada classe base se assemelha a um campo membro (na verdade, eles são dispostos na RAM dessa maneira), fornecendo um pouco de açúcar sintático e um capacidade extra de substituir mais métodos virtuais. Isso pode impor alguma ambiguidade no tempo de compilação, mas geralmente é fácil de resolver.
Por outro lado, com a herança virtual, ela sai do controle com muita facilidade (e depois se torna uma bagunça). Considere como exemplo um diagrama de "coração":
Em C ++ é totalmente impossível: assim que
F
eG
são mesclados em uma única classe, seusA
s também são mesclados, ponto final. Isso significa que você nunca pode considerar classes de base opaco em C ++ (neste exemplo, você tem que construirA
emH
que você tem que saber que presente em algum lugar na hierarquia). Em outros idiomas, pode funcionar, no entanto; por exemplo,F
eG
poderia declarar explicitamente A como "interno", proibindo assim a consequente fusão e efetivamente se solidificando.Outro exemplo interessante ( não específico do C ++):
Aqui, apenas
B
usa herança virtual. Portanto,E
contém doisB
s que compartilham o mesmoA
. Dessa forma, é possível obter umA*
ponteiro que aponte paraE
, mas não é possível convertê-lo em umB*
ponteiro, embora o objeto seja realmenteB
ambíguo e essa ambiguidade não possa ser detectada em tempo de compilação (a menos que o compilador veja o programa inteiro). Aqui está o código do teste:Além disso, a implementação pode ser muito complexa (depende da linguagem; veja a resposta de benjismith).
fonte