Pergunta

Vejo pessoas perguntando o tempo todo se a herança múltipla deve ser incluída na próxima versão do C# ou Java.O pessoal de C++, que tem a sorte de ter essa habilidade, diz que isso é como dar a alguém uma corda para eventualmente se enforcar.

Qual é o problema com herança múltipla?Existem amostras de concreto?

Foi útil?

Solução

O problema mais óbvio é com a substituição da função.

Digamos ter duas aulas A e B, ambos os que definem um método doSomething. Agora você define uma terceira classe C, que herda de ambos A e B, mas você não substitui o doSomething método.

Quando o compilador semeia este código ...

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

... Qual implementação do método ele deve usar? Sem nenhum esclarecimento adicional, é impossível para o compilador resolver a ambiguidade.

Além de substituir, o outro grande problema com a herança múltipla é o layout dos objetos físicos na memória.

Idiomas como C ++ e Java e C# criam um layout baseado em endereço fixo para cada tipo de objeto. Algo assim:

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

Quando o compilador gera código da máquina (ou bytecode), ele usa essas compensações numéricas para acessar cada método ou campo.

A herança múltipla torna muito complicado.

Se aula C herda de ambos A e B, o compilador deve decidir se deve fazer o layout dos dados em AB ordem ou em BA ordem.

Mas agora imagine que você está chamando métodos em um B objeto. É realmente apenas um B? Ou é realmente um C objeto sendo chamado polimorficamente, através de seu B interface? Dependendo da identidade real do objeto, o layout físico será diferente e é impossível saber o deslocamento da função a ser invocado no site de chamada.

A maneira de lidar com esse tipo de sistema é abandonar a abordagem de layout fixa, permitindo que cada objeto seja consultado para seu layout antes da tentando invocar as funções ou acessar seus campos.

Então ... longa história curta ... é uma dor no pescoço para os autores do compilador apoiarem a herança múltipla. Então, quando alguém como o Guido Van Rossum projeta Python, ou quando Anders Hejlsberg designs C#, eles sabem que o suporte à herança múltipla tornará as implementações do compilador significativamente mais complexas e, presumivelmente, não acham que o benefício vale o custo.

Outras dicas

Os problemas que vocês mencionam não são realmente tão difíceis de resolver. De fato, por exemplo, Eiffel faz isso perfeitamente bem! (e sem introduzir escolhas arbitrárias ou o que quer que seja)

Por exemplo, se você herdar de A e B, ambos com método foo (), é claro que você não quer uma escolha arbitrária em sua classe C herdando de A e B. Você precisa redefinir o Foo, então fica claro o que será Usado se C.Foo () for chamado ou de outra forma você precisar 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, perdi o recurso quando tive que voltar à programação em Java.

O problema do diamante:

Uma ambiguidade que surge quando duas classes B e C herdam de A e Classe D herda de B e C. Se houver um método em A que B e C têm substituído, e D não o substitui, então qual versão do método d herda: a de B, ou a de C?

... É chamado de "problema de diamante" por causa da forma do diagrama de herança de classe nessa situação. Nesse caso, a classe A está no topo, B e C separadamente abaixo dela, e D se junta aos dois no fundo para formar uma forma de diamante ...

A herança múltipla é uma daquelas coisas que não são usadas com frequência e podem ser mal utilizadas, mas às vezes é necessário.

Eu nunca entendi não adicionar um recurso, apenas porque 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 que você aplique pré -condições ou pós -condições. Assim como qualquer outra ferramenta, você precisa saber quando é apropriado usar e como usá -la.

Digamos que você tenha objetos A e B, que são herdados por C. A e B, ambos implementam Foo () e C não. Eu chamo C.Foo (). Qual implementação é escolhida? Existem outros problemas, mas esse tipo de coisa é grande.

O principal problema com a herança múltipla é bem resumido com o exemplo de Tloach. 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 precisa fazer aqui é bastante arbitrário, isso torna o código muito menos intuitivo.

Acho que, ao fazer um bom design de oo, nunca preciso de herança múltipla. Nos casos, preciso disso, geralmente acho que uso a herança para reutilizar a funcionalidade enquanto a herança é apropriada apenas para as relações "IS-A".

Existem outras técnicas, como o Mixins, que resolvem os mesmos problemas e não têm os problemas que a herança múltipla tem.

Não acho que o problema do diamante seja um problema, eu consideraria esse sofisticado, nada mais.

O pior problema, do meu ponto de vista, com a herança múltipla são as vítimas e as pessoas que afirmam ser desenvolvedores, mas, na realidade, estão presas com a metade - conhecimento (na melhor das hipóteses).

Pessoalmente, eu ficaria muito feliz se finalmente pudesse fazer algo em formas do Windows como essa (não é um código correto, mas deve lhe dar a ideia):

public sealed class CustomerEditView : Form, MVCView<Customer>

Esta é a questão principal que tenho sem ter herança múltipla. Você pode fazer algo semelhante com interfaces, mas há o que eu chamo de "code s ***", é esse doloroso c *** 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 a menor, para qualquer repetição de código em uma língua moderna.

O Sistema de Objetos Lisp Comum Lisp (CLOF) é outro exemplo de algo que suporta mi, evitando os problemas de estilo C ++: a herança é dada um Padrão sensível, enquanto ainda permite que você a liberdade de decidir explicitamente como exatamente, digamos, chame o comportamento de um super.

Não há nada errado na própria herança múltipla. 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 linguagem Eiffel está suportando herança múltipla sem restrições de maneira muito eficiente e produtiva, mas o idioma foi projetado a partir desse começo para apoiá -lo.

Esse recurso é complexo para 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 poderia evitar o suporte de outros recursos (ou seja, não é necessária a necessidade de interface ou método de extensão).

Eu acho que apoiar ou não o suporte de múltiplas herança é mais uma questão de escolha, uma questão de prioridades. Um recurso mais complexo leva mais tempo para ser implementado corretamente e operacional e pode ser mais controverso. A implementação do C ++ pode ser a razão pela qual a herança múltipla não foi implementada em C# e Java ...

Um dos objetivos de design de estruturas como Java e .NET é tornar possível que o código compilado funcione com uma versão de uma biblioteca pré-compilada, para funcionar igualmente bem com versões subsequentes dessa biblioteca, mesmo que essas versões subsequentes adicione novos recursos.Embora o paradigma normal em linguagens como C ou C++ seja distribuir executáveis ​​vinculados estaticamente que contenham todas as bibliotecas necessárias, o paradigma em .NET e Java é distribuir aplicativos como coleções de componentes que são "vinculados" em 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 definia 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 equivalia a declarar uma classe como implementando a interface da outra e exigia que a nova classe reimplementasse todos os membros públicos das classes das quais ela derivou.Se Y e Z derivarem de X, e então W derivar de Y e Z, não importará se Y e Z implementam os membros de X de maneira diferente, porque Z não será capaz de usar suas implementações – ele terá que definir suas implementações. ter.W poderia encapsular instâncias de Y e/ou Z e encadear suas implementações dos métodos de X através das deles, mas não haveria ambigüidade quanto ao que os métodos de X deveriam fazer - eles fariam tudo o que o código de Z explicitamente os instruísse a fazer.

A dificuldade em Java e .NET é que o código pode herdar membros e ter acesso a eles implicitamente referem-se aos membros pais.Suponha que alguém tenha classes W-Z relacionadas como acima:

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();
  }
}

Pareceria que W.Test() deve criar uma instância de W chamar a implementação do método virtual Foo definido em X.Suponha, entretanto, que Y e Z estivessem realmente em um módulo compilado separadamente e, embora tenham sido definidos como acima quando X e W foram compilados, eles foram posteriormente alterados e recompilados:

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

Agora, qual deve ser o efeito de chamar W.Test()?Se o programa tivesse que ser vinculado estaticamente antes da distribuição, o estágio de link estático poderia ser capaz de discernir que, embora o programa não tivesse ambiguidade antes de Y e Z serem alterados, as alterações em Y e Z tornaram as coisas ambíguas e o vinculador poderia se recusar a construir o programa a menos ou até que tal 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 deseja executar o programa e não possui código-fonte para nada dele.Quando W.Test() é executado, não seria mais claro o que W.Test() deveria fazer, mas até que o usuário tentasse executar W com a nova versão de Y e Z, não haveria nenhuma maneira de qualquer parte do sistema reconhecer que havia um problema (a menos que W fosse considerado ilegítimo mesmo antes das mudanças em Y e Z) .

O diamante não é um problema, desde que você não Use qualquer coisa como a herança virtual C ++: na herança normal, cada classe base se assemelha a um campo de membro (na verdade eles são dispostos em RAM dessa maneira), oferecendo um pouco de açúcar sintático e uma capacidade extra de substituir os métodos mais virtuais. Isso pode impor alguma ambiguidade em tempo de compilação, mas geralmente é fácil de resolver.

Por outro lado, com a herança virtual, ele fica muito facilmente fora de controle (e depois se torna uma bagunça). Considere como exemplo um diagrama de "coração":

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

Em C ++ é totalmente impossível: assim que F e G são mesclados em uma única classe, seus AS também são mesclados, ponto final. Isso significa que você nunca pode considerar as classes base opacas em C ++ (neste exemplo, você deve construir A dentro H Então você precisa saber que isso presente em algum lugar da hierarquia). Em outras línguas, no entanto, pode funcionar; por exemplo, F e G poderia declarar explicitamente um "interno", proibindo a fusão consequente e efetivamente se tornando sólida.

Outro exemplo interessante (não C ++-específico):

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

Somente aqui B usa herança virtual. Então E contém dois BS que compartilham o mesmo A. Dessa forma, você pode obter um A* ponteiro que aponta para E, mas você não pode lançar para um B* ponteiro embora o objeto é na realidade B Como tal, o elenco é ambíguo, e essa ambiguidade não pode ser detectada no momento da compilação (a menos que o compilador veja todo o programa). Aqui está o código de teste:

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;
}

Além disso, a implementação pode ser muito complexa (depende da linguagem; veja a resposta de Benjismith).

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top