Pregunta

Puedo ver a personas que preguntan todo el tiempo si se debe incluir la herencia múltiple en la próxima versión de C # o Java. La gente de C ++, que es lo suficientemente afortunada como para tener esta habilidad, dice que esto es como darle a alguien una cuerda para que eventualmente se cuelgue.

¿Qué pasa con la herencia múltiple? ¿Hay muestras concretas?

¿Fue útil?

Solución

El problema más obvio es con la función que invalida.

Supongamos que tenemos dos clases A y B , las cuales definen un método doSomething . Ahora define un C de tercera clase, que hereda tanto de A como de B , pero no anula el doSomething método.

Cuando el compilador genere este código ...

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

... ¿Qué implementación del método debería usar? Sin más aclaraciones, es imposible que el compilador resuelva la ambigüedad.

Además de anular, el otro gran problema con la herencia múltiple es el diseño de los objetos físicos en la memoria.

Los lenguajes como C ++ y Java y C # crean un diseño fijo basado en direcciones para cada tipo de objeto. Algo como esto:

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

Cuando el compilador genera un código de máquina (o un código de bytes), usa esos desplazamientos numéricos para acceder a cada método o campo.

La herencia múltiple hace que sea muy difícil.

Si la clase C se hereda tanto de A como de B , el compilador debe decidir si distribuir los datos en AB order o en BA .

Pero ahora imagine que está llamando métodos en un objeto B . ¿Es realmente solo un B ? ¿O es realmente un objeto C que se llama polimórficamente, a través de su interfaz B ? Dependiendo de la identidad real del objeto, el diseño físico será diferente, y es imposible conocer el desplazamiento de la función a invocar en el sitio de la llamada.

La manera de manejar este tipo de sistema es deshacerse del enfoque de diseño fijo, permitiendo que cada objeto sea consultado para su diseño antes intentando invocar las funciones o acceder a sus campos.

Entonces ... cuento, es un dolor en el cuello para los autores del compilador que admiten la herencia múltiple. Entonces, cuando alguien como Guido van Rossum diseña python, o cuando Anders Hejlsberg diseña c #, saben que admitir la herencia múltiple hará que las implementaciones del compilador sean mucho más complejas, y probablemente no creen que el beneficio valga la pena el costo.

Otros consejos

Los problemas que mencionan no son realmente tan difíciles de resolver. De hecho, por ejemplo. Eiffel hace eso perfectamente bien! (y sin introducir opciones arbitrarias o lo que sea)

Por ejemplo, Si hereda de A y B, ambos tienen el método foo (), entonces, por supuesto, no desea una elección arbitraria en su clase C heredada de A y B. Debe redefinir foo para que quede claro qué se utilizará si se llama a c.foo () o, de lo contrario, debe cambiar el nombre de uno de los métodos en C. (podría convertirse en barra ())

También creo que la herencia múltiple suele ser bastante útil. Si miras las bibliotecas de Eiffel, verás que se usa en todas partes y, personalmente, me he perdido la función cuando tuve que volver a la programación en Java.

El problema del diamante :

  

una ambigüedad que surge cuando dos clases B y C heredan de A, y la clase D hereda de B y C. Si hay un método en A, B y C tienen invalidado , y D no la reemplaza, entonces ¿qué versión del método hereda D: la de B, o la de C?

     

... Se llama el " problema del diamante " debido a la forma del diagrama de herencia de clase en esta situación. En este caso, la clase A está en la parte superior, tanto B como C por separado debajo de ella, y D une las dos en la parte inferior para formar una forma de diamante ...

La herencia múltiple es una de esas cosas que no se usa a menudo, y se puede usar mal, pero a veces es necesaria.

Nunca entendí no agregar una función, solo porque podría ser mal utilizada, cuando no hay buenas alternativas. Las interfaces no son una alternativa a la herencia múltiple. Por un lado, no le permiten imponer condiciones previas o posteriores. Al igual que cualquier otra herramienta, debe saber cuándo es apropiado usarla y cómo usarla.

digamos que tiene los objetos A y B, ambos heredados por C. A y B implementan foo () y C no. Llamo a C.foo (). ¿Qué implementación se elige? Hay otros problemas, pero este tipo de cosas es grande.

El problema principal con la herencia múltiple se resume muy bien con el ejemplo de tloach. Cuando se hereda de varias clases base que implementan la misma función o campo, el compilador tiene que tomar una decisión sobre qué implementación heredar.

Este empeoramiento cuando se hereda de varias clases que se heredan de la misma clase base. (herencia de diamante, si dibuja el árbol de herencia obtiene una forma de diamante)

Estos problemas no son realmente problemáticos para que un compilador los supere. Pero la elección que tiene que hacer el compilador aquí es bastante arbitraria, esto hace que el código sea mucho menos intuitivo.

Encuentro que cuando hago un buen diseño de OO nunca necesito herencia múltiple. En los casos en que lo necesito, generalmente encuentro que he estado utilizando la herencia para reutilizar la funcionalidad, mientras que la herencia solo es apropiada para " is-a " relaciones.

Hay otras técnicas, como los mixins, que resuelven los mismos problemas y no tienen los problemas que tiene la herencia múltiple.

No creo que el problema del diamante sea un problema, consideraría ese sofisma, nada más.

El peor problema, desde mi punto de vista, con herencia múltiple es RAD: las víctimas y las personas que afirman ser desarrolladores, pero en realidad están estancadas con la mitad del conocimiento (como mucho).

Personalmente, estaría muy contento si finalmente pudiera hacer algo en Windows Forms como este (no es el código correcto, pero debería darle una idea):

public sealed class CustomerEditView : Form, MVCView<Customer>

Este es el problema principal que tengo al no tener herencia múltiple. PUEDES hacer algo similar con las interfaces, pero hay lo que yo llamo "código ***", es este c repetitivo y doloroso que tienes que escribir en cada una de tus clases para obtener un contexto de datos, por ejemplo.

En mi opinión, no debería haber ninguna necesidad, ni la más mínima, de NINGUNA repetición de código en un lenguaje moderno.

El Sistema de objetos Common Lisp (CLOS) es otro ejemplo de algo que admite MI y evita los problemas de estilo C ++: la herencia se otorga a sensible default , a la vez que le permite la libertad de decidir explícitamente cómo, por ejemplo, llamar al comportamiento de un super.

No hay nada de malo en la herencia múltiple en sí. El problema es agregar herencia múltiple a un idioma que no fue diseñado teniendo en cuenta la herencia múltiple desde el principio.

El lenguaje Eiffel admite la herencia múltiple sin restricciones de una manera muy eficiente y productiva, pero el lenguaje se diseñó desde el principio para admitirlo.

Esta característica es compleja de implementar para los desarrolladores de compiladores, pero parece que ese inconveniente podría ser compensado por el hecho de que un buen soporte de herencia múltiple podría evitar el soporte de otras características (es decir, no es necesaria la Interfaz o el Método de Extensión). / p>

Creo que apoyar la herencia múltiple o no es más una cuestión de elección, una cuestión de prioridades. Una característica más compleja lleva más tiempo para ser implementada y operativa correctamente y puede ser más controvertida. La implementación de C ++ puede ser la razón por la que no se implementó la herencia múltiple en C # y Java ...

Uno de los objetivos de diseño de marcos como Java y .NET es hacer posible que el código compilado funcione con una versión de una biblioteca precompilada, que funcione igualmente bien con versiones posteriores de esa biblioteca, incluso si Esas versiones posteriores agregan nuevas características. Mientras que el paradigma normal en lenguajes como C o C ++ es distribuir ejecutables enlazados estáticamente que contienen todas las bibliotecas que necesitan, el paradigma en .NET y Java es distribuir aplicaciones como colecciones de componentes que están vinculados " enlazados " en tiempo de ejecución.

El modelo COM que precedió a .NET intentó usar este enfoque general, pero en realidad no tenía herencia; en su lugar, cada definición de clase definía efectivamente una clase y una interfaz del mismo nombre que contenía a todos sus miembros públicos . Las instancias eran del tipo de clase, mientras que las referencias eran del tipo de interfaz. Declarar una clase como derivada de otra era equivalente a declarar que una clase implementaba la interfaz de la otra, y requería que la nueva clase reimplementara a todos los miembros públicos de las clases de las que derivaba. Si Y y Z se derivan de X, y luego W se deriva de Y y Z, no importará si Y y Z implementan los miembros de X de manera diferente, ya que Z no podrá usar sus implementaciones, tendrá que definir su propio. W podría encapsular instancias de Y y / o Z, y encadenar sus implementaciones de los métodos de X a través de los suyos, pero no habría ambigüedad en cuanto a lo que deberían hacer los métodos de X; harían lo que el código de Z explícitamente les indicara. / p>

La dificultad en Java y .NET es que el código puede heredar miembros y tener acceso a ellos implícitamente se refiere a los miembros principales. Supongamos que uno de ellos tuviera clases W-Z relacionadas como anteriormente:

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

Parece que W.Test () debería crear una instancia de W para llamar a la implementación del método virtual Foo definido en X . Sin embargo, supongamos que Y y Z estaban en realidad en un módulo compilado por separado, y aunque se definieron como anteriormente cuando se compilaron X y W, luego se cambiaron y se volvieron a compilar:

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

Ahora, ¿cuál debería ser el efecto de llamar a W.Test () ? Si el programa tenía que vincularse estáticamente antes de la distribución, la etapa del enlace estático podría discernir que, si bien el programa no tenía ambigüedad antes de que se cambiaran Y y Z, los cambios en Y y Z han hecho que las cosas sean ambiguas y el vinculador podría negarse. construir el programa a menos o hasta que se resuelva tal ambigüedad. Por otro lado, es posible que la persona que tiene tanto W como las nuevas versiones de Y y Z sea alguien que simplemente quiera ejecutar el programa y no tenga código fuente para ninguno de ellos. Cuando se ejecuta W.Test () , ya no quedará claro qué debe hacer W.Test () , sino hasta que el usuario intente ejecutar W con la nueva versión de Y y Z no habría forma de que ninguna parte del sistema pudiera reconocer que había un problema (a menos que W fuera considerado ilegítimo incluso antes de los cambios en Y y Z).

El diamante no es un problema, siempre que no & # 8217; t use algo como la herencia virtual de C ++: en la herencia normal, cada clase base se parece a un campo miembro (en realidad, están dispuestas en RAM de esta manera), que le da un poco de azúcar sintáctica y una capacidad adicional para anular más métodos virtuales. Eso puede imponer cierta ambigüedad en tiempo de compilación, pero eso generalmente es fácil de resolver.

Por otro lado, con la herencia virtual, con demasiada facilidad queda fuera de control (y luego se convierte en un desastre). Considere como ejemplo un & # 8220; corazón & # 8221; diagrama:

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

En C ++ es completamente imposible: tan pronto como F y G se combinan en una sola clase, sus A también se fusionan , punto Eso significa que nunca puede considerar opacas las clases base en C ++ (en este ejemplo, debe construir A en H , por lo que debe saber que está presente en algún lugar de la jerarquía). En otros idiomas puede funcionar, sin embargo; por ejemplo, F y G podrían declarar explícitamente A como & # 8220; internal, & # 8221; por lo tanto, se prohíbe la fusión consecuente y la consolidación efectiva de los mismos.

Otro ejemplo interesante ( no específico de C ++):

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

Aquí, solo B usa la herencia virtual. Así que E contiene dos B que comparten el mismo A . De esta manera, puede obtener un puntero A * que apunta a E , pero no puede & # 8217; t convertirlo en un puntero B * aunque el objeto es en realidad B , ya que dicho lanzamiento es ambiguo, y esta ambigüedad no se puede detectar en tiempo de compilación (a menos que el compilador vea todo el programa). Aquí está el código de prueba:

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

Además, la implementación puede ser muy compleja (depende del idioma; consulte la respuesta de benjismith & # 8217; s).

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top