La herencia múltiple es una característica de algunos lenguajes de programación informática orientados a objetos en la que un objeto o una clase pueden heredar características de más de un objeto o una clase padre . Es distinta de la herencia simple, en la que un objeto o una clase solo pueden heredar de un objeto o una clase en particular.
La herencia múltiple ha sido un tema controvertido durante muchos años, [1] [2] con oponentes que señalan su creciente complejidad y ambigüedad en situaciones como el "problema del diamante", donde puede ser ambiguo en cuanto a qué clase padre se hereda una característica particular si más de una clase padre implementa dicha característica. Esto se puede abordar de varias maneras, incluido el uso de herencia virtual . [3] También se han propuesto métodos alternativos de composición de objetos que no se basan en la herencia, como mixins y rasgos , para abordar la ambigüedad.
Detalles
En programación orientada a objetos (POO), la herencia describe una relación entre dos clases en la que una clase (la clase hija ) es una subclase de la clase padre . La clase hija hereda los métodos y atributos de la clase padre, lo que permite compartir la funcionalidad. Por ejemplo, se podría crear una clase variable Mammal con características como comer, reproducirse, etc.; luego, definir una clase hija Cat que herede esas características sin tener que programarlas explícitamente, mientras agrega nuevas características como perseguir ratones .
La herencia múltiple permite a los programadores utilizar más de una jerarquía totalmente ortogonal simultáneamente, como por ejemplo permitir que Gato herede de Personaje de dibujos animados , Mascota y Mamífero y acceda a características desde dentro de todas esas clases.
Implementaciones
Los lenguajes que admiten herencia múltiple incluyen: C++ , Common Lisp (a través de Common Lisp Object System (CLOS)), EuLisp (a través de The EuLisp Object System TELOS), Curl , Dylan , Eiffel , Logtalk , Object REXX , Scala (a través del uso de clases mixin ), OCaml , Perl , POP-11 , Python , R , Raku y Tcl (integrado desde 8.6 o a través de Incremental Tcl ( Incr Tcl ) en versiones anteriores [4] [5] ).
El entorno de ejecución de IBM System Object Model (SOM) admite herencia múltiple, y cualquier lenguaje de programación orientado a SOM puede implementar nuevas clases SOM heredadas de múltiples bases.
Algunos lenguajes orientados a objetos, como Swift , Java , Fortran desde su revisión de 2003 , C# y Ruby implementan herencia única , aunque los protocolos o interfaces proporcionan algunas de las funciones de la verdadera herencia múltiple.
PHP utiliza clases de rasgos para heredar implementaciones de métodos específicos. Ruby utiliza módulos para heredar múltiples métodos.
El problema del diamante

El " problema del diamante " (a veces denominado el "diamante mortal de la muerte" [6] ) es una ambigüedad que surge cuando dos clases B y C heredan de A, y la clase D hereda tanto de B como de C. Si hay un método en A que B y C han anulado , y D no lo anula, entonces ¿qué versión del método hereda D: la de B o la de C?
Por ejemplo, en el contexto del desarrollo de software de GUI , una clase puede heredar de ambas clases (para la apariencia) y (para la funcionalidad/manejo de entrada), y las clases y ambas heredan de la clase. Ahora bien, si se llama al método para un objeto y no existe dicho método en la clase, pero hay un método anulado en o (o en ambas), ¿qué método debería ser el que se llame finalmente?
ButtonRectangleClickableRectangleClickableObjectequalsButtonButtonequalsRectangleClickable
Se denomina "problema del diamante" debido a la forma del diagrama de herencia de clases 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 un diamante.
Mitigación
Los idiomas tienen diferentes formas de abordar estos problemas de herencia repetida.
- C# (desde C# 8.0) permite la implementación de métodos de interfaz predeterminados, lo que hace que una clase
Aque implementa interfacesIayIbcon métodos similares que tienen implementaciones predeterminadas tenga dos métodos "heredados" con la misma firma, lo que causa el problema del diamante. Se mitiga ya sea al requerirAla implementación del método en sí, eliminando así la ambigüedad, o al forzar al llamador a convertir primero elAobjeto a la interfaz adecuada para usar su implementación predeterminada de ese método (por ejemplo,((Ia) aInstance).Method();). - C++ sigue por defecto cada ruta de herencia por separado, por lo que un
Dobjeto en realidad contendría dosAobjetos separados, y los usos deAlos miembros de tienen que ser calificados correctamente. Si la herencia deAaBy la herencia deAaCestán ambas marcadas como "virtual" (por ejemplo, "class B : virtual public A"), C++ tiene especial cuidado de crear solo unAobjeto, y los usos deAlos miembros de funcionan correctamente. Si se mezclan la herencia virtual y la herencia no virtual, hay un único virtualAy un no virtualApara cada ruta de herencia no virtual aA. C++ requiere indicar explícitamente desde qué clase padre se invoca la característica que se va a utilizar, es decirWorker::Human.Age, . C++ no admite la herencia repetida explícita, ya que no habría forma de calificar qué superclase utilizar (es decir, que una clase aparezca más de una vez en una única lista de derivación [class Dog : public Animal, Animal]). C++ también permite crear una única instancia de la clase múltiple a través del mecanismo de herencia virtual (es decir,Worker::HumanyMusician::Humanhará referencia al mismo objeto). - Common Lisp CLOS intenta proporcionar tanto un comportamiento predeterminado razonable como la capacidad de anularlo. De forma predeterminada, para decirlo de forma sencilla, los métodos se ordenan en
D,B,C,A, cuando B se escribe antes de C en la definición de clase. Se elige el método con las clases de argumentos más específicas (D>(B,C)>A); luego en el orden en que se nombran las clases padre en la definición de subclase (B>C). Sin embargo, el programador puede anular esto, dando un orden de resolución de método específico o indicando una regla para combinar métodos. Esto se llama combinación de métodos, que puede controlarse completamente. El MOP ( protocolo de metaobjetos ) también proporciona medios para modificar la herencia, el envío dinámico , la instanciación de clases y otros mecanismos internos sin afectar la estabilidad del sistema. - Curl permite que solo las clases que están explícitamente marcadas como compartidas se hereden repetidamente. Las clases compartidas deben definir un constructor secundario para cada constructor regular de la clase. El constructor regular se llama la primera vez que se inicializa el estado de la clase compartida a través de un constructor de subclase, y el constructor secundario se invocará para todas las demás subclases.
- En Eiffel , las características de los ancestros se eligen explícitamente con directivas select y rename. Esto permite que las características de la clase base se compartan entre sus descendientes o dar a cada uno de ellos una copia separada de la clase base. Eiffel permite la unión o separación explícita de características heredadas de clases antecesoras. Eiffel unirá automáticamente las características, si tienen el mismo nombre e implementación. El escritor de clases tiene la opción de cambiar el nombre de las características heredadas para separarlas. La herencia múltiple es una ocurrencia frecuente en el desarrollo de Eiffel; la mayoría de las clases efectivas en la biblioteca EiffelBase de estructuras de datos y algoritmos ampliamente utilizada, por ejemplo, tienen dos o más padres. [7]
- Go evita el problema del diamante en tiempo de compilación. Si una estructura
Dincorpora dos estructurasByCque tienen un métodoF(), por lo que satisfacen una interfazA, el compilador se quejará de un "selector ambiguo" siD.F()se llama a , o siDse asigna una instancia de a una variable de tipoA. Los métodos deByCse pueden llamar explícitamente conD.B.F()oD.C.F(). - Java 8 introduce métodos predeterminados en las interfaces. Si
A,B,Cson interfaces,B,Ccada una puede proporcionar una implementación diferente a un método abstracto deA, lo que provoca el problema del diamante. Cualquiera de las clasesDdebe reimplementar el método (cuyo cuerpo puede simplemente reenviar la llamada a una de las superimplementaciones), o la ambigüedad será rechazada como un error de compilación. [8] Antes de Java 8, Java no estaba sujeto al riesgo del problema del diamante, porque no admitía herencia múltiple y los métodos predeterminados de la interfaz no estaban disponibles. - El script JavaFX en la versión 1.2 permite la herencia múltiple mediante el uso de mixins . En caso de conflicto, el compilador prohíbe el uso directo de la variable o función ambigua. Se puede seguir accediendo a cada miembro heredado mediante la conversión del objeto al mixin de interés, por ejemplo
(individual as Person).printInfo();. - Kotlin permite la herencia múltiple de interfaces, sin embargo, en un escenario de problema de Diamante, la clase hija debe anular el método que causa el conflicto de herencia y especificar qué implementación de clase padre se debe utilizar, por ejemplo
super<ChosenParentInterface>.someMethod() - Logtalk admite herencia múltiple tanto de interfaz como de implementación, lo que permite la declaración de alias de métodos que proporcionan tanto cambio de nombre como acceso a métodos que quedarían enmascarados por el mecanismo de resolución de conflictos predeterminado.
- En OCaml , las clases padre se especifican individualmente en el cuerpo de la definición de clase. Los métodos (y atributos) se heredan en el mismo orden, y cada nuevo método heredado anula cualquier método existente. OCaml elige la última definición coincidente de una lista de herencia de clases para resolver qué implementación de método utilizar en caso de ambigüedades. Para anular el comportamiento predeterminado, simplemente se califica una llamada de método con la definición de clase deseada.
- Perl utiliza la lista de clases de las que heredar como una lista ordenada. El compilador utiliza el primer método que encuentra mediante una búsqueda en profundidad de la lista de superclases o utilizando la linealización C3 de la jerarquía de clases. Varias extensiones proporcionan esquemas alternativos de composición de clases. El orden de herencia afecta a la semántica de la clase. En la ambigüedad anterior, la clase
By sus antecesores se comprobarían antes que la claseCy sus antecesores, por lo que el método enAse heredaría a través deB. Esto se comparte con Io y Picolisp . En Perl, este comportamiento se puede anular utilizandomrou otros módulos para utilizar la linealización C3 u otros algoritmos. [9] - Python tiene la misma estructura que Perl, pero, a diferencia de Perl, la incluye en la sintaxis del lenguaje. El orden de herencia afecta la semántica de las clases. Python tuvo que lidiar con esto al introducir clases de nuevo estilo, todas las cuales tienen un ancestro común,
object. Python crea una lista de clases utilizando el algoritmo de linealización C3 (o Orden de Resolución de Métodos (MRO)). Ese algoritmo impone dos restricciones: los hijos preceden a sus padres y si una clase hereda de múltiples clases, se mantienen en el orden especificado en la tupla de clases base (sin embargo, en este caso, algunas clases altas en el gráfico de herencia pueden preceder a las clases más bajas en el gráfico [10] ). Por lo tanto, el orden de resolución de métodos es:D,B,C,A. [11] - Las clases Ruby tienen exactamente un padre, pero también pueden heredar de varios módulos; las definiciones de clase Ruby se ejecutan y la (re)definición de un método oculta cualquier definición existente previamente en el momento de la ejecución. En ausencia de metaprogramación en tiempo de ejecución, esto tiene aproximadamente la misma semántica que la resolución en profundidad más a la derecha.
- Scala permite la creación de instancias múltiples de rasgos , lo que permite la herencia múltiple al agregar una distinción entre la jerarquía de clases y la jerarquía de rasgos. Una clase solo puede heredar de una sola clase, pero puede mezclar tantos rasgos como desee. Scala resuelve los nombres de los métodos utilizando una búsqueda en profundidad de "rasgos" extendidos, primero a la derecha, antes de eliminar todas las ocurrencias de cada módulo excepto la última en la lista resultante. Por lo tanto, el orden de resolución es: [
D,C,A,B,A], que se reduce a [D,C,B,A]. - Tcl permite múltiples clases padre; el orden de especificación en la declaración de clase afecta la resolución de nombres para los miembros que utilizan el algoritmo de linealización C3 . [12]
Los lenguajes que solo permiten una herencia simple , donde una clase solo puede derivar de una clase base, no tienen el problema del diamante. La razón de esto es que dichos lenguajes tienen como máximo una implementación de cualquier método en cualquier nivel de la cadena de herencia independientemente de la repetición o ubicación de los métodos. Normalmente, estos lenguajes permiten que las clases implementen múltiples protocolos , llamados interfaces en Java. Estos protocolos definen métodos pero no proporcionan implementaciones concretas. Esta estrategia ha sido utilizada por ActionScript , C# , D , Java , Nemerle , Object Pascal , Objective-C , Smalltalk , Swift y PHP . [13] Todos estos lenguajes permiten que las clases implementen múltiples protocolos.
Además, Ada , C#, Java, Object Pascal, Objective-C, Swift y PHP permiten la herencia múltiple de interfaces (llamadas protocolos en Objective-C y Swift). Las interfaces son como clases base abstractas que especifican firmas de métodos sin implementar ningún comportamiento. (Las interfaces "puras" como las de Java hasta la versión 7 no permiten ninguna implementación o datos de instancia en la interfaz). Sin embargo, incluso cuando varias interfaces declaran la misma firma de método, tan pronto como ese método se implementa (define) en cualquier parte de la cadena de herencia, anula cualquier implementación de ese método en la cadena superior (en sus superclases). Por lo tanto, en cualquier nivel dado en la cadena de herencia, puede haber como máximo una implementación de cualquier método. Por lo tanto, la implementación de un método de herencia única no presenta el Problema de Diamante incluso con herencia múltiple de interfaces. Con la introducción de la implementación predeterminada para interfaces en Java 8 y C# 8, aún es posible generar un Problema de Diamante, aunque esto solo aparecerá como un error de tiempo de compilación.
Véase también
Referencias
- ^ Cargill, TA (invierno de 1991). "Controversia: el caso contra la herencia múltiple en C++". Sistemas informáticos . 4 (1): 69–82.
- ^ Waldo, Jim (primavera de 1991). "Controversia: el caso de la herencia múltiple en C++". Sistemas informáticos . 4 (2): 157–171.
- ^ Schärli, Nathanael; Ducasse, Stéphane; Nierstrasz, Oscar; Black, Andrew. "Rasgos: unidades componibles de comportamiento" (PDF) . Web.cecs.pdx.edu . Consultado el 21 de octubre de 2016 .
- ^ "incr Tcl". blog.tcl.tk . Consultado el 14 de abril de 2020 .
- ^ "Introducción al lenguaje de programación Tcl". www2.lib.uchicago.edu . Consultado el 14 de abril de 2020 .
- ^ Martin, Robert C. (9 de marzo de 1997). "Java y C++: una comparación crítica" (PDF) . Objectmentor.com . Archivado desde el original (PDF) el 24 de octubre de 2005. Consultado el 21 de octubre de 2016 .
- ^ "Estándar ECMA-367". Ecma-international.org . Consultado el 21 de octubre de 2016 .
- ^ "Estado de Lambda". Cr.openjdk.java.net . Consultado el 21 de octubre de 2016 .
- ^ "perlobj". perldoc.perl.org . Consultado el 21 de octubre de 2016 .
- ^ Resumen. "El orden de resolución de métodos de Python 2.3". Python.org . Consultado el 21 de octubre de 2016 .
- ^ "Unificación de tipos y clases en Python 2.2". Python.org . Consultado el 21 de octubre de 2016 .
- ^ "Página de manual de la clase". Tcl.tk . 16 de noviembre de 1999 . Consultado el 21 de octubre de 2016 .
- ^ "Interfaces de objetos - Manual". PHP.net . 2007-07-04 . Consultado el 2016-10-21 .
Lectura adicional
- Stroustrup, Bjarne (1999). Herencia múltiple para C++. Actas de la Conferencia del Grupo de Usuarios de Unix Europeo de primavera de 1987
- Construcción de software orientado a objetos , segunda edición, de Bertrand Meyer , Prentice Hall, 1997, ISBN 0-13-629155-4
- Eddy Truyen; Wouter Joosen; Bo Nørregaard; Pierre Verbaeten (2004). "Una generalización y solución al problema del dilema del ancestro común en sistemas de objetos basados en delegación" (PDF) . Actas del taller sobre aspectos dinámicos de 2004 (103–119).
- Ira R. Forman; Scott Danforth (1999). Poner las metaclases en práctica . ISBN 0-201-43305-2.
Enlaces externos
- Tutorial sobre el uso de la herencia en Eiffel
- Tutorial sobre el uso efectivo de la herencia múltiple en Python
- Una visión general de la herencia en Ocaml
