Puedo ver personas preguntando todo el tiempo si se debe incluir la herencia múltiple en la próxima versión de C # o Java. Las personas de C ++, que tienen la suerte de tener esta habilidad, dicen que esto es como darle una cuerda a alguien para que se ahorque.
¿Qué pasa con la herencia múltiple? ¿Hay alguna muestra de concreto?
oop
multiple-inheritance
language-theory
Vlad Gudim
fuente
fuente
Respuestas:
El problema más obvio es con la anulación de funciones.
Digamos que tiene dos clases
A
yB
, las cuales definen un métododoSomething
. Ahora define una tercera claseC
, que hereda de ambosA
yB
, pero no anula eldoSomething
método.Cuando el compilador inicia este código ...
... ¿Qué implementación del método debería usar? Sin ninguna otra aclaración, 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.
Lenguajes como C ++ y Java y C # crean un diseño fijo basado en direcciones para cada tipo de objeto. Algo como esto:
Cuando el compilador genera código de máquina (o código de bytes), utiliza esos desplazamientos numéricos para acceder a cada método o campo.
La herencia múltiple lo hace muy complicado.
Si la clase
C
hereda de ambosA
yB
, el compilador tiene que decidir si desea diseñar los datos enAB
orden o enBA
orden.Pero ahora imagina que estás llamando métodos a un
B
objeto. ¿Es realmente solo unB
? ¿O es realmente unC
objeto que se llama polimórficamente, a través de suB
interfaz? Dependiendo de la identidad real del objeto, el diseño físico será diferente, y es imposible saber el desplazamiento de la función para invocar en el sitio de la llamada.La forma de manejar este tipo de sistema es deshacerse del enfoque de diseño fijo, permitiendo que cada objeto sea consultado por su diseño antes de intentar invocar las funciones o acceder a sus campos.
Entonces ... larga historia corta ... es un dolor en el cuello para los autores del compilador admitir 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 significativamente más complejas, y presumiblemente no creen que el beneficio valga la pena.
fuente
Los problemas que ustedes mencionan no son realmente tan difíciles de resolver. De hecho, por ejemplo, Eiffel lo hace 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 heredando de A y B. Debe redefinir foo para que quede claro qué será se usa 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 bar ())
También creo que la herencia múltiple a menudo es bastante útil. Si miras las bibliotecas de Eiffel, verás que se usa por todas partes y, personalmente, me perdí la función cuando tuve que volver a programar en Java.
fuente
El problema del diamante :
fuente
someZ
y quiere lanzarloObject
y luegoB
? ¿CuálB
conseguirá?Object
y de nuevo a ese tipo ...La herencia múltiple es una de esas cosas que no se usa con frecuencia, y puede ser mal utilizada, 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 hacer cumplir precondiciones o postcondiciones. Al igual que cualquier otra herramienta, debe saber cuándo es apropiado usarlo y cómo usarlo.
fuente
assert
?Supongamos que tiene 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.
fuente
El principal problema con la herencia múltiple se resume muy bien con el ejemplo de tloach. Al heredar de múltiples clases base que implementan la misma función o campo, es el compilador el que debe tomar una decisión sobre qué implementación heredar.
Esto empeora cuando hereda de varias clases que heredan de la misma clase base. (herencia de diamantes, si dibujas el árbol de herencia obtienes 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 OO nunca necesito herencia múltiple. En los casos en que lo necesito, generalmente encuentro que he estado usando la herencia para reutilizar la funcionalidad, mientras que la herencia solo es apropiada para las relaciones "es-a".
Existen otras técnicas como los mixins que resuelven los mismos problemas y no tienen los problemas que tiene la herencia múltiple.
fuente
([..bool..]? "test": 1)
?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 - víctimas y personas que dicen ser desarrolladores pero en realidad están atrapados con medio conocimiento (en el mejor de los casos).
Personalmente, estaría muy feliz si finalmente pudiera hacer algo en Windows Forms como este (no es el código correcto, pero debería darle la idea):
Este es el principal problema 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 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 absolutamente ninguna necesidad, ni la más mínima, de CUALQUIER repetición de código en un lenguaje moderno.
fuente
El Common Lisp Object System (CLOS) es otro ejemplo de algo que soporta MI mientras evita los problemas de estilo C ++: la herencia tiene un valor predeterminado sensato , mientras que aún le da la libertad de decidir explícitamente cómo, exactamente, llamar al comportamiento de un super .
fuente
No hay nada malo en la herencia múltiple en sí. El problema es agregar herencia múltiple a un lenguaje que no fue diseñado teniendo en cuenta la herencia múltiple desde el principio.
El lenguaje Eiffel admite herencia múltiple sin restricciones de una manera muy eficiente y productiva, pero el lenguaje fue diseñado desde ese principio para admitirlo.
Esta característica es compleja de implementar para los desarrolladores de compiladores, pero parece que ese inconveniente podría compensarse por el hecho de que un buen soporte de herencia múltiple podría evitar el soporte de otras características (es decir, sin necesidad de Interfaz o Método de Extensión).
Creo que apoyar o no la herencia múltiple es más una cuestión de elección, una cuestión de prioridades. Una característica más compleja requiere más tiempo para implementarse correctamente y ser operativa, y puede ser más controvertida. La implementación de C ++ puede ser la razón por la cual no se implementó la herencia múltiple en C # y Java ...
fuente
Uno de los objetivos de diseño de frameworks como Java y .NET es hacer posible que el código compilado funcione con una versión de una biblioteca precompilada, para que funcione igual de bien con versiones posteriores de esa biblioteca, incluso si esas versiones posteriores Añadir nuevas funciones. Si bien el paradigma normal en lenguajes como C o C ++ es distribuir ejecutables vinculados 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" 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 cambio, 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 que una clase deriva de otra era equivalente a declarar que una clase implementaba la interfaz de la otra, y requería que la nueva clase volviera a implementar todos los miembros públicos de las clases de las que se derivaba. Si Y y Z derivan de X, y luego W deriva de Y y Z, no importará si Y y Z implementan los miembros de X de manera diferente, porque Z no podrá usar sus implementaciones, tendrá que definir su propio. W podría encapsular instancias de Y y / o Z,
La dificultad en Java y .NET es que el código puede heredar miembros y tener acceso a ellos se refiere implícitamente a los miembros principales. Supongamos que uno tuviera clases relacionadas con WZ como arriba:
Parece
W.Test()
que crear una instancia de W llama a la implementación del método virtualFoo
definido enX
. Supongamos, sin embargo, que Y y Z en realidad estaban 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:Ahora, ¿cuál debería ser el efecto de llamar
W.Test()
? Si el programa tuviera que estar enlazado estáticamente antes de la distribución, la etapa de enlace estático podría ser capaz de 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 las cosas ambiguas y el enlazador podría negarse a construir el programa a menos o hasta que se resuelva dicha ambigüedad. Por otro lado, es posible que la persona que tiene W y las nuevas versiones de Y y Z sea alguien que simplemente quiere ejecutar el programa y no tiene código fuente para ninguno de ellos. Cuando seW.Test()
ejecuta, ya no estaría claro quéW.Test()
debería hacerlo, pero 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) .fuente
El diamante no es un problema, siempre y cuando no use nada como la herencia virtual de C ++: en la herencia normal, cada clase base se asemeja a un campo miembro (en realidad, se presentan en la RAM de esta manera), lo que le da un poco de azúcar sintáctica y un capacidad adicional para anular más métodos virtuales. Eso puede imponer cierta ambigüedad en tiempo de compilación, pero eso suele ser fácil de resolver.
Por otro lado, con la herencia virtual, se sale de control con demasiada facilidad (y luego se convierte en un desastre). Considere como ejemplo un diagrama de "corazón":
En C ++ es totalmente imposible: tan pronto como sea
F
yG
se fusionan en una sola clase, susA
s se fusionan demasiado, y punto. Eso significa que nunca puede considerar que las clases base sean opacas en C ++ (en este ejemplo, debe construir,A
porH
lo que debe saber que está presente en algún lugar de la jerarquía). Sin embargo, en otros idiomas puede funcionar; por ejemplo,F
yG
podría declarar explícitamente A como "interno", lo que prohíbe la fusión consiguiente y se hace efectivamente sólido.Otro ejemplo interesante ( no específico de C ++):
Aquí, solo
B
usa herencia virtual. EntoncesE
contiene dosB
s que comparten lo mismoA
. De esta manera, puede obtener unA*
puntero que apuntaE
, pero no puede lanzarlo a unB*
puntero, aunque el objeto es realmenteB
como tal el lanzamiento es ambiguo, y esta ambigüedad no se puede detectar en el momento de la compilación (a menos que el compilador vea el programa completo). Aquí está el código de prueba:Además, la implementación puede ser muy compleja (depende del lenguaje; ver la respuesta de benjismith).
fuente