El envío múltiple o multimétodos es una característica de algunos lenguajes de programación en los que una función o método se puede enviar dinámicamente en función del tipo de tiempo de ejecución (dinámico) o, en el caso más general, algún otro atributo de más de uno de sus argumentos . [1] Esta es una generalización del polimorfismo de envío único donde una llamada a una función o método se envía dinámicamente en función del tipo derivado del objeto en el que se ha llamado al método. El envío múltiple enruta el envío dinámico a la función o método de implementación utilizando las características combinadas de uno o más argumentos.
Los desarrolladores de software informático suelen organizar el código fuente en bloques con nombre, denominados subrutinas , procedimientos, subprogramas, funciones o métodos. El código de la función se ejecuta invocándola , es decir, ejecutando un fragmento de código que hace referencia a su nombre . Esto transfiere el control temporalmente a la función llamada; cuando la ejecución de la función ha finalizado, el control normalmente se transfiere de nuevo a la instrucción en la función que llama que sigue a la referencia.
Los nombres de las funciones suelen seleccionarse de manera que describan el propósito de la función. A veces es conveniente dar a varias funciones el mismo nombre, a menudo porque realizan tareas conceptualmente similares, pero operan con diferentes tipos de datos de entrada. En tales casos, la referencia del nombre en el sitio de llamada de la función no es suficiente para identificar el bloque de código que se va a ejecutar. En cambio, también se utilizan la cantidad y el tipo de argumentos de la llamada de la función para seleccionar entre varias implementaciones de funciones.
En lenguajes de programación más convencionales, es decir, orientados a objetos de despacho único , cuando se invoca un método ( enviar un mensaje en Smalltalk , llamar a una función miembro en C++ ), uno de sus argumentos se trata de manera especial y se utiliza para determinar cuál de las (potencialmente muchas) clases de métodos de ese nombre se aplicará. En muchos lenguajes, el argumento especial se indica sintácticamente; por ejemplo, varios lenguajes de programación colocan el argumento especial antes de un punto al realizar una llamada a un método: special.method(other, arguments, here)
, de modo que lion.sound()
produciría un rugido, mientras que sparrow.sound()
produciría un chirrido.
En cambio, en lenguajes con envío múltiple, el método seleccionado es simplemente aquel cuyos argumentos coinciden con el número y tipo de la llamada de función. No hay ningún argumento especial que sea propietario de la función/método ejecutado en una llamada en particular.
El Common Lisp Object System (CLOS) es un ejemplo temprano y bien conocido de despacho múltiple. Otro ejemplo notable del uso del despacho múltiple es el lenguaje de programación Julia .
El envío múltiple debe distinguirse de la sobrecarga de funciones , en la que se utiliza información de tipado estático, como el tipo declarado o inferido de un término (o el tipo base en un lenguaje con subtipado) para determinar cuál de varias posibilidades se utilizará en un sitio de llamada determinado, y esa determinación se realiza en el momento de la compilación o el enlace (o en algún otro momento antes de que comience la ejecución del programa) y, a partir de entonces, es invariable para una implementación o ejecución determinada del programa. Muchos lenguajes como C++ ofrecen una sobrecarga de funciones robusta, pero no ofrecen un envío múltiple dinámico (C++ solo permite el envío único dinámico mediante el uso de funciones virtuales).
Cuando se trabaja con lenguajes que pueden discriminar tipos de datos en tiempo de compilación , puede ocurrir que se seleccione entre las alternativas. El acto de crear dichas funciones alternativas para la selección en tiempo de compilación se suele denominar sobrecarga de una función.
En los lenguajes de programación que posponen la identificación del tipo de datos hasta el momento de ejecución (es decir, enlace tardío ), la selección entre funciones alternativas debe ocurrir entonces, en función de los tipos de argumentos de función determinados dinámicamente. Las funciones cuyas implementaciones alternativas se seleccionan de esta manera se denominan, en general, multimétodos .
Existe un cierto costo de tiempo de ejecución asociado con el envío dinámico de llamadas de función. En algunos lenguajes, [ cita requerida ] la distinción entre sobrecarga y métodos múltiples puede ser borrosa, y el compilador determina si la selección en tiempo de compilación se puede aplicar a una llamada de función dada o si se necesita un envío más lento en tiempo de ejecución.
Existen varios problemas conocidos con el envío dinámico, tanto simple como múltiple. Si bien muchos de estos problemas se resuelven con el envío simple, que ha sido una característica estándar en los lenguajes de programación orientados a objetos durante décadas, estos problemas se vuelven más complicados en el caso del envío múltiple.
En la mayoría de los lenguajes de programación más populares, el código fuente se entrega y se implementa en gránulos de funcionalidad que aquí llamaremos paquetes ; la terminología real para este concepto varía según el lenguaje. Cada paquete puede contener múltiples definiciones de tipo, valor y función; los paquetes a menudo se compilan por separado en lenguajes con un paso de compilación y puede existir una relación de dependencia no cíclica. Un programa completo es un conjunto de paquetes, con un paquete principal que puede depender de varios otros paquetes, y el programa completo consiste en el cierre transitivo de la relación de dependencia.
El llamado problema de expresión se relaciona con la capacidad del código en un paquete dependiente de extender comportamientos (funciones o tipos de datos) definidos en un paquete base desde dentro de un paquete incluido, sin modificar la fuente del paquete base. Los lenguajes orientados a objetos tradicionales de despacho único hacen que sea trivial agregar nuevos tipos de datos pero no nuevas funciones; los lenguajes funcionales tradicionales tienden a tener el efecto opuesto, y el despacho múltiple, si se implementa correctamente, permite ambos. Es deseable que una implementación de despacho múltiple tenga las siguientes propiedades:
En general, es deseable que para cualquier invocación dada de un método múltiple, haya como máximo un candidato "mejor" entre los casos de implementación del método múltiple, y/o que si no lo hay, que esto se resuelva de una manera predecible y determinista, incluyendo el fracaso. El comportamiento no determinista es indeseable. Suponiendo un conjunto de tipos con una relación de subtipos no circular, se puede definir que una implementación de un método múltiple es "mejor" (más específica) si todos los argumentos distribuidos dinámicamente en el primero son subtipos de todos los argumentos distribuidos dinámicamente especificados en el segundo, y al menos uno es un subtipo estricto. Con un único envío y en ausencia de herencia múltiple , esta condición se satisface trivialmente, pero con un envío múltiple, es posible que dos o más candidatos satisfagan una lista de argumentos real dada, pero ninguno es más específico que el otro (un argumento dinámico es el subtipo en un caso, otro es el subtipo en el otro caso). Esto puede suceder particularmente si dos paquetes diferentes, ninguno de los cuales depende del otro, extienden algún método múltiple con implementaciones relacionadas con los tipos de cada paquete, y luego un tercer paquete que incluye ambos (posiblemente indirectamente) invoca el método múltiple usando argumentos de ambos paquetes.
Las posibles resoluciones incluyen:
La implementación eficiente del envío único, incluso en lenguajes de programación que se compilan por separado en código objeto y se vinculan con un enlazador de bajo nivel (no consciente del lenguaje), incluso de manera dinámica en el momento de carga/inicio del programa o incluso bajo la dirección del código de la aplicación, es bien conocida. El método " vtable " desarrollado en C++ y otros lenguajes OO tempranos (donde cada clase tiene una matriz de punteros de función correspondientes a las funciones virtuales de esa clase) es casi tan rápido como una llamada de método estático, requiriendo una sobrecarga de O(1) y solo una búsqueda de memoria adicional incluso en el caso no optimizado. Sin embargo, el método vtable usa el nombre de la función y no el tipo de argumento como su clave de búsqueda, y no se escala al caso de envío múltiple. (También depende del paradigma orientado a objetos de los métodos que son características de las clases, no entidades independientes de cualquier tipo de datos en particular).
La implementación eficiente del despacho múltiple sigue siendo un problema de investigación en curso.
Para estimar la frecuencia con la que se utiliza el envío múltiple en la práctica, Muschevici et al. [2] estudiaron programas que utilizan el envío dinámico. Analizaron nueve aplicaciones, en su mayoría compiladores, escritos en seis lenguajes diferentes: Common Lisp Object System , Dylan , Cecil , MultiJava, Diesel y Nice. Sus resultados muestran que entre el 13 y el 32 % de las funciones genéricas utilizan el tipo dinámico de un argumento, mientras que entre el 2,7 y el 6,5 % de ellas utilizan el tipo dinámico de múltiples argumentos. El 65-93 % restante de las funciones genéricas tienen un método concreto (anulador) y, por lo tanto, no se considera que utilicen los tipos dinámicos de sus argumentos. Además, el estudio informa que entre el 2 y el 20 % de las funciones genéricas tenían dos y entre el 3 y el 6 % tenían tres implementaciones de funciones concretas. Los números disminuyen rápidamente para las funciones con anuladores más concretos.
El despacho múltiple se utiliza mucho más intensamente en Julia , donde el despacho múltiple fue un concepto de diseño central desde el origen del lenguaje: recopilando las mismas estadísticas que Muschevici sobre el número promedio de métodos por función genérica, se encontró que la biblioteca estándar de Julia utiliza más del doble de la cantidad de sobrecarga que en los otros lenguajes analizados por Muschevici, y más de 10 veces en el caso de los operadores binarios . [3]
Los datos de estos artículos se resumen en la siguiente tabla, donde la tasa de despacho DR
es el número promedio de métodos por función genérica; la tasa de elección CR
es la media del cuadrado del número de métodos (para medir mejor la frecuencia de funciones con una gran cantidad de métodos); [2] [3] y el grado de especialización DoS
es el número promedio de argumentos especializados en tipo por método (es decir, el número de argumentos que se envían):
La teoría de lenguajes de despacho múltiple fue desarrollada por primera vez por Castagna et al., al definir un modelo para funciones sobrecargadas con enlace tardío . [4] [5] Produjo la primera formalización del problema de covarianza y contravarianza de lenguajes orientados a objetos [6] y una solución al problema de métodos binarios. [7]
La distinción entre envíos múltiples y envíos únicos puede hacerse más clara con un ejemplo. Imaginemos un juego que tiene, entre sus objetos (visibles para el usuario), naves espaciales y asteroides. Cuando dos objetos colisionan, el programa puede tener que hacer cosas diferentes según qué acaba de chocar con qué.
C# introdujo soporte para métodos múltiples dinámicos en la versión 4 [8] (abril de 2010) mediante la palabra clave 'dynamic'. El siguiente ejemplo demuestra métodos múltiples. Al igual que muchos otros lenguajes de tipado estático, C# también admite la sobrecarga de métodos estáticos. [9] Microsoft espera que los desarrolladores elijan el tipado estático en lugar del tipado dinámico en la mayoría de los escenarios. [10] La palabra clave 'dynamic' admite la interoperabilidad con objetos COM y lenguajes .NET de tipado dinámico.
El siguiente ejemplo utiliza características introducidas en C# 9 y C# 10.
utilizando ColliderLibrary estático ; Consola.WriteLine ( Colisionar ( nuevo Asteroide ( 101 ) , nueva Nave Espacial ( 300 ))); Consola.WriteLine ( Colisionar ( nuevo Asteroide ( 10 ) , nueva Nave Espacial ( 10 ) )); Consola.WriteLine ( Colisionar ( nueva Nave Espacial ( 101 ), nueva Nave Espacial ( 10 ) )); string Collide ( SpaceObject x , SpaceObject y ) => x . Tamaño > 100 && y . Tamaño > 100 ? "¡Gran explosión!" : CollideWith ( x como dinámico , y como dinámico ); // Envío dinámico al método CollideWith clase ColliderLibrary { cadena estática pública CollideWith ( Asteroide x , Asteroide y ) => "a/a" ; cadena estática pública CollideWith ( Asteroide x , Nave espacial y ) => "a/s" ; cadena estática pública CollideWith ( Nave espacial x , Asteroide y ) => "s/a" ; cadena estática pública CollideWith ( Nave espacial x , Nave espacial y ) => "s/s" ; } registro abstracto SpaceObject ( int Tamaño ); registro Asteroide ( int Tamaño ) : SpaceObject ( Tamaño ); registro Nave espacial ( int Tamaño ) : SpaceObject ( Tamaño );
Producción:
¡Gran boom! a/s s/s
Groovy es un lenguaje JVM compatible e interutilizable con Java de propósito general que, a diferencia de Java, utiliza enlace tardío/despacho múltiple. [11]
/* Implementación maravillosa del ejemplo de C# anterior. El enlace tardío funciona de la misma manera cuando se usan métodos no estáticos o se compilan clases/métodos estáticamente (anotación @CompileStatic) */ class Program { static void main ( String [] args ) { println Collider . collide ( new Asteroid ( 101 ), new Spaceship ( 300 )) println Collider . collide ( new Asteroid ( 10 ), new Spaceship ( 10 )) println Collider . collide ( new Spaceship ( 101 ), new Spaceship ( 10 )) } } clase Collider { static String collide ( SpaceObject x , SpaceObject y ) { ( x . size > 100 && y . size > 100 ) ? "big-boom" : collideWith ( x , y ) // Envío dinámico al método collideWith } Cadena estática privada collideWith ( Asteroide x , Asteroide y ) { "a/a" } Cadena estática privada collideWith ( Asteroide x , Nave espacial y ) { "a/s" } Cadena estática privada collideWith ( Nave espacial x , Asteroide y ) { "s/a" } Cadena estática privada collideWith ( Nave espacial x , Nave espacial y ) { "s/s" } } clase SpaceObject { int tamaño SpaceObject ( int tamaño ) { this . tamaño = tamaño } } @InheritConstructors clase Asteroide extiende SpaceObject {} @InheritConstructors clase Nave espacial extiende SpaceObject {}
En un lenguaje con envío múltiple, como Common Lisp , podría parecerse más a esto (ejemplo de Common Lisp mostrado):
( defmethod colisionar-con (( x asteroide ) ( y asteroide )) ;; lidiar con asteroide golpeando asteroide ) ( defmethod colisionar-con (( x asteroide ) ( y nave espacial )) ;; lidiar con asteroide golpeando nave espacial ) ( defmethod colisionar-con (( x nave espacial ) ( y asteroide )) ;; lidiar con nave espacial golpeando asteroide ) ( defmethod colisionar-con (( x nave espacial ) ( y nave espacial )) ;; lidiar con nave espacial golpeando nave espacial )
Y lo mismo ocurre con los demás métodos. No se utilizan pruebas explícitas ni "conversión dinámica".
En presencia de múltiples envíos, la idea tradicional de que los métodos están definidos en clases y contenidos en objetos se vuelve menos atractiva: cada método de colisión anterior está asociado a dos clases diferentes, no a una. Por lo tanto, la sintaxis especial para la invocación de métodos generalmente desaparece, de modo que la invocación de métodos se ve exactamente como la invocación de funciones ordinarias y los métodos se agrupan no en clases sino en funciones genéricas .
Julia tiene incorporado un envío múltiple, que es fundamental para el diseño del lenguaje. [3] La versión de Julia del ejemplo anterior podría verse así:
tipo abstracto SpaceObject fin struct Asteroide <: SpaceObject tamaño :: Int fin struct Nave espacial <: SpaceObject tamaño :: Int fin colisionar_con ( :: Asteroide , :: Nave espacial ) = "a/s" colisionar_con ( :: Nave espacial , :: Asteroide ) = "s/a" colisionar_con ( :: Nave espacial , :: Nave espacial ) = "s/s" colisionar_con ( :: Asteroide , :: Asteroide ) = "a/a" colisionar ( x :: ObjetoEspacial , y :: ObjetoEspacial ) = ( x.tamaño > 100 && y.tamaño > 100 ) ? " ¡ Gran explosión ! " : colisionar_con ( x , y )
Producción:
julia> colisionar ( Asteroide ( 101 ), Nave espacial ( 300 )) "¡Gran explosión!" Julia> colisionar ( Asteroide ( 10 ), Nave espacial ( 10 )) "a/s" Julia> colisionar ( Nave espacial ( 101 ), Nave espacial ( 10 )) "s/s"
Raku , al igual que Perl, utiliza ideas probadas de otros lenguajes, y los sistemas de tipos han demostrado ofrecer ventajas convincentes en el análisis de código del lado del compilador y una semántica poderosa del lado del usuario a través del envío múltiple.
Tiene tanto métodos múltiples como subprogramas múltiples. Como la mayoría de los operadores son subrutinas, también tiene múltiples operadores despachados.
Junto con las restricciones de tipo habituales, también tiene restricciones where que permiten realizar subrutinas muy especializadas.
subconjunto Masa de Real donde 0 ^..^ Inf ; rol Stellar-Object { tiene Masa $.mass es requerido ; nombre del método () devuelve Str {...}; }clase Asteroide hace Stellar-Object { nombre del método () { 'un asteroide' }}clase Nave espacial hace Stellar-Object { tiene Str $.name = 'alguna nave espacial sin nombre' ;}mi Str @destroyed = < obliterado destruido destrozado >; mi Str @damaged = « dañado 'chocó con' 'fue dañado por' »;# Agregamos candidatos múltiples a los operadores de comparación numérica porque los estamos comparando numéricamente, # pero no tiene sentido que los objetos se conviertan a un tipo numérico. # ( Si se convirtieran no necesariamente necesitaríamos agregar estos operadores. ) # También podríamos haber definido operadores completamente nuevos de la misma manera. multi sub infix: « <=> » ( Stellar-Object:D $a , Stellar-Object:D $b ) { $a . mass <=> $b . mass } multi sub infix: « < » ( Stellar-Object:D $a , Stellar-Object:D $b ) { $a . mass < $b . mass } multi sub infix: « > » ( Stellar-Object:D $a , Stellar-Object:D $b ) { $a . mass > $b . masa } multi sub infijo: « == » ( Objeto-estelar:D $a , Objeto-estelar:D $b ) { $a . masa == $b . masa }# Defina un nuevo despachador múltiple y agregue algunas restricciones de tipo a los parámetros. # Si no lo definimos, habríamos obtenido uno genérico que no tenía restricciones. proto sub collide ( Stellar-Object:D $, Stellar-Object:D $ ) {*}# No es necesario repetir los tipos aquí ya que son los mismos que el prototipo. # La restricción 'where' técnicamente solo se aplica a $b, no a toda la firma. # Tenga en cuenta que la restricción 'where' usa el candidato de operador `<` que agregamos anteriormente. multi sub collide ( $a , $b where $a < $b ) { say "$a.name() fue @destroyed.pick() por $b.name()" ;} colisión de múltiples subs ( $a , $b donde $a > $b ) { # reenviar al candidato anterior con los argumentos intercambiados igual que $b , $a ; }# Esto tiene que estar después de los dos primeros porque los otros # tienen restricciones 'where', que se verifican en el # orden en que se escribieron los subs. (Este siempre coincidiría). multi sub collide ( $a , $b ) { # aleatoriza el orden my ( $n1 , $n2 ) = ( $a . name , $b . name ). pick (*); say "$n1 @damaged.pick() $n2" ;}# Los siguientes dos candidatos pueden estar en cualquier lugar después del proto, # porque tienen tipos más especializados que los tres anteriores.# Si las naves tienen masas desiguales, se llama a uno de los dos primeros candidatos. multi sub collide ( Spaceship $a , Spaceship $b where $a == $b ){ my ( $n1 , $n2 ) = ( $a . name , $b . name ). pick (*); say "$n1 chocó con $n2, y ambas naves estaban " , ( @destroyed . pick , 'dejado dañado' ). pick ;}# Puede descomprimir los atributos en variables dentro de la firma. # Incluso podría tener una restricción sobre ellos `(:mass($a) donde 10)`. multi sub collide ( Asteroid $ (: mass ( $a )), Asteroid $ (: mass ( $b )) ){ diga "dos asteroides chocaron y se combinaron en un asteroide más grande de masa { $a + $b }" ;}mi nave espacial $Enterprise .= new (: masa ( 1 ),: nombre ( 'La Enterprise' )); colisionar Asteroide . new (: masa ( .1 )), $Enterprise ; colisionar $Enterprise , Nave espacial . new (: masa ( .1 )); colisionar $Enterprise , Asteroide . new (: masa ( 1 )); colisionar $Enterprise , Nave espacial . new (: masa ( 1 )); colisionar Asteroide . new (: masa ( 10 )), Asteroide . new (: masa ( 5 ));
En los lenguajes que no admiten el envío múltiple a nivel de definición del lenguaje o sintáctico, a menudo es posible agregar el envío múltiple mediante una extensión de biblioteca . JavaScript y TypeScript no admiten métodos múltiples a nivel de sintaxis, pero es posible agregar el envío múltiple mediante una biblioteca. Por ejemplo, el paquete multimethod [12] proporciona una implementación de funciones genéricas de envío múltiple.
Versión tipada dinámicamente en JavaScript:
importar { multi , método } desde '@arrows/multimethod' clase Asteroide {} clase Nave espacial {} const collideWith = multi ( method ([ Asteroide , Asteroide ], ( x , y ) => { // lidiar con el choque de asteroide }), method ([ Asteroide , Nave espacial ], ( x , y ) => { // lidiar con el choque de asteroide }), method ([ Nave espacial , Asteroide ], ( x , y ) => { // lidiar con el choque de nave espacial }), method ([ Nave espacial , Nave espacial ], ( x , y ) => { // lidiar con el choque de nave espacial }), )
Versión tipada estáticamente en TypeScript:
importar { multi , método , Multi } desde '@arrows/multimethod' clase Asteroide {} clase Nave espacial {} tipo CollideWith = Multi & { ( x : Asteroide , y : Asteroide ) : void ( x : Asteroide , y : Nave espacial ) : void ( x : Nave espacial , y : Asteroide ) : void ( x : Nave espacial , y : Nave espacial ) : void } const collideWith : CollideWith = multi ( method ([ Asteroide , Asteroide ], ( x , y ) => { // lidiar con el choque de asteroide }), method ([ Asteroide , Nave espacial ], ( x , y ) => { // lidiar con el choque de asteroide }), method ([ Nave espacial , Asteroide ], ( x , y ) => { // lidiar con el choque de nave espacial }), method ([ Nave espacial , Nave espacial ], ( x , y ) => { // lidiar con el choque de nave espacial }), )
Se puede agregar envío múltiple a Python usando una extensión de biblioteca . Por ejemplo, usando el módulo multimethod.py [13] y también con el módulo multimethods.py [14] que proporciona métodos múltiples de estilo CLOS para Python sin cambiar la sintaxis subyacente o las palabras clave del lenguaje.
desde multimethods importa Dispatch desde game_objects importa Asteroide , Nave espacial desde game_behaviors importa as_func , ss_func , sa_funccolisionar = Dispatch () colisionar.add_rule ( ( Asteroide , Nave espacial ), as_func ) colisionar.add_rule ( ( Nave espacial , Nave espacial ) , ss_func ) colisionar.add_rule (( Nave espacial , Asteroide ) , sa_func ) def aa_func ( a , b ): """Comportamiento cuando un asteroide choca con otro asteroide.""" # ...define un nuevo comportamiento... colisionar . add_rule (( Asteroide , Asteroide ), aa_func )
# ...más tarde... colisionan ( cosa1 , cosa2 )
Funcionalmente, esto es muy similar al ejemplo CLOS, pero la sintaxis es Python convencional.
Utilizando decoradores de Python 2.4 , Guido van Rossum produjo una implementación de muestra de métodos múltiples [15] con una sintaxis simplificada:
@multimethod ( Asteroid , Asteroid ) def collide ( a , b ): """Comportamiento cuando un asteroide choca con otro asteroide.""" # ...define un nuevo comportamiento... @multimethod ( Asteroid , Spaceship ) def collide ( a , b ): """Comportamiento cuando un asteroide choca con una nave espacial.""" # ...define un nuevo comportamiento... # ...define otras reglas multimétodo...
y luego pasa a definir el decorador multimétodo.
El paquete PEAK-Rules proporciona envío múltiple con una sintaxis similar al ejemplo anterior. [16] Posteriormente fue reemplazado por PyProtocols. [17]
La biblioteca Reg también admite el envío múltiple y de predicados. [18]
Con la introducción de sugerencias de tipo , es posible realizar envíos múltiples con una sintaxis aún más simple. Por ejemplo, si se utiliza plum-dispatch,
Desde el despacho de importación de ciruelas @dispatch def collide ( a : Asteroide , b : Asteroide ): """Comportamiento cuando un asteroide choca con otro asteroide.""" # ...define nuevo comportamiento... @dispatch def collide ( a : Asteroide , b : Nave espacial ): """Comportamiento cuando un asteroide choca con una nave espacial.""" # ...define nuevo comportamiento... #...definir más reglas...
C no tiene distribución dinámica, por lo que debe implementarse manualmente de alguna forma. A menudo se utiliza una enumeración para identificar el subtipo de un objeto. La distribución dinámica se puede realizar buscando este valor en una tabla de ramificación de puntero de función . Aquí hay un ejemplo simple en C:
typedef void ( * Caso de colisión )( void ); void colisión_AA ( void ) { /* manejar colisión asteroide-asteroide */ }; void colisión_AS ( void ) { /* manejar colisión asteroide-nave espacial */ }; void colisión_SA ( void ) { /* manejar colisión nave espacial-asteroide */ }; void colisión_SS ( void ) { /* manejar colisión nave espacial-nave espacial*/ }; typedef enum { THING_ASTEROID = 0 , THING_SPACESHIP , THING_COUNT /* no es un tipo de cosa en sí, en su lugar se usa para encontrar cantidad de cosas */ } Cosa ; CollisionCase colisiónCases [ COSA_COUNT ][ COSA_COUNT ] = { { & colisión_AA , & colisión_AS }, { & colisión_SA , & colisión_SS } }; void colisionar ( Cosa a , Cosa b ) { ( * colisiónCases [ a ][ b ])(); } int main ( void ) { colisionar ( COSA_NAVE ESPACIAL , COSA_ASTEROIDE ); }
Con la biblioteca C Object System, [19] C admite el envío dinámico similar a CLOS. Es totalmente extensible y no necesita ningún manejo manual de los métodos. Los mensajes dinámicos (métodos) son enviados por el despachador de COS, que es más rápido que Objective-C. Aquí hay un ejemplo en COS:
#include <stdio.h> #include <cos/Object.h> #include <cos/gen/object.h> // clasesdefclass ( Asteroide ) // miembros de datos endclass defclass ( Spaceship ) // miembros de datos endclass // genéricosdefgeneric ( _Bool , colisionar_con , _1 , _2 ); // multimétodosdefmethod ( _Bool , colisionar_con , asteroide , asteroide ) // lidiar con el impacto de un asteroide endmethod defmethod ( _Bool , colisionar_con , asteroide , nave espacial ) // lidiar con el asteroide que choca con la nave espacial endmethod defmethod ( _Bool , colisionar_con , nave espacial , asteroide ) // lidiar con la nave espacial que choca con un asteroide endmethod defmethod ( _Bool , colisionar_con , Nave espacial , Nave espacial ) // lidiar con el choque de una nave espacial con otra nave espacial endmethod // ejemplo de usoint main ( void ) { OBJ a = gnew ( Asteroide ); OBJ s = gnew ( Nave espacial ); printf ( "<a,a> = %d \n " , colisionar_con ( a , a )); printf ( "<a,s> = %d \n " , colisionar_con ( a , s )); printf ( "<s,a> = %d \n " , colisionar_con ( s , a )); printf ( "<s,s> = %d \n " , colisionar_con ( s , s )); liberación ( a ); liberación ( s ); }
A partir de 2021 [actualizar], C++ admite de forma nativa solo un envío único, aunque Bjarne Stroustrup (y colaboradores) propusieron agregar métodos múltiples (envío múltiple) en 2007. [20] Los métodos para evitar este límite son análogos: use el patrón de visitante , la conversión dinámica o una biblioteca:
// Ejemplo que utiliza la comparación de tipos en tiempo de ejecución mediante dynamic_cast estructura Cosa { virtual void collideWith ( Cosa & otro ) = 0 ; }; struct Asteroid : Thing { void collideWith ( Thing & other ) { // dynamic_cast a un tipo de puntero devuelve NULL si la conversión falla // (dynamic_cast a un tipo de referencia lanzaría una excepción en caso de falla) if ( auto asteroid = dynamic_cast < Asteroid *> ( & other )) { // manejar la colisión asteroide-asteroide } else if ( auto spaceship = dynamic_cast < Spaceship *> ( & other )) { // manejar la colisión asteroide-nave espacial } else { // manejo de colisiones predeterminado aquí } } }; struct Nave espacial : Cosa { void collideWith ( Cosa y otra ) { if ( auto asteroid = dynamic_cast < Asteroide *> ( y otra )) { // manejar la colisión Nave espacial-Asteroide } else if ( auto nave espacial = dynamic_cast < Nave espacial *> ( y otra )) { // manejar la colisión Nave espacial-Nave espacial } else { // manejo de colisiones predeterminado aquí } } };
o tabla de búsqueda de puntero a método:
#include <cstdint> #include <información_de_tipo> #include <mapa_desordenado> clase Cosa { protegida : Cosa ( std :: uint32_t cid ) : tid ( cid ) {} const std :: uint32_t tid ; // tipo id typedef void ( Cosa ::* CollisionHandler )( Cosa y otros ); typedef std :: mapa_desordenado < std :: uint64_t , CollisionHandler > CollisionHandlerMap ; void estático addHandler ( std :: uint32_t id1 , std :: uint32_t id2 , CollisionHandler handler ) { CollisionCases.insert ( CollisionHandlerMap :: value_type ( key ( id1 , id2 ) , handler )) ; } estático std :: uint64_t key ( std :: uint32_t id1 , std :: uint32_t id2 ) { return std :: uint64_t ( id1 ) << 32 | id2 ; } CollisionHandlerMap estático colisiónCases ; público : void collideWith ( Thing & other ) { auto handler = clashCases . find ( key ( tid , other . tid )); if ( handler != clashCases . end ()) { ( this ->* handler -> second )( other ); // llamada de puntero a método } else { // manejo de colisiones predeterminado } } }; clase Asteroide : público Cosa { void asteroid_collision ( Cosa y otros ) { /*manejar colisión asteroide-asteroide*/ } void spaceship_collision ( Cosa y otros ) { /*manejar colisión asteroide-nave espacial*/ } público : Asteroide () : Cosa ( cid ) {} static void initCases (); static const std :: uint32_t cid ; }; clase Nave espacial : pública Cosa { void asteroid_collision ( Cosa y otros ) { /*manejar colisión nave espacial-asteroide*/ } void spaceship_collision ( Cosa y otros ) { /*manejar colisión nave espacial-nave espacial*/ } público : Nave espacial () : Cosa ( cid ) {} static void initCases (); static const std :: uint32_t cid ; // id de clase }; Cosa :: CollisionHandlerMap Cosa :: CollisionCases ; const std :: uint32_t Asteroide :: cid = typeid ( Asteroide ). hash_code () ; const std :: uint32_t Nave espacial :: cid = typeid ( Nave espacial ). hash_code (); void Asteroide::initCases () { addHandler ( cid , cid , CollisionHandler ( & Asteroide :: colisión_de_asteroides )); addHandler ( cid , Nave espacial :: cid , CollisionHandler ( & Asteroide :: colisión_de_nave_espacial )); } void Nave Espacial::initCases () { addHandler ( cid , Asteroide :: cid , CollisionHandler ( & Nave Espacial :: asteroid_collision )); addHandler ( cid , cid , CollisionHandler ( & Nave Espacial :: spaceship_collision )); } int main () { Asteroide :: initCases (); Nave espacial :: initCases (); Asteroide a1 , a2 ; Nave espacial s1 , s2 ; a1 .colisionarCon ( a2 ) ; a1 .colisionarCon ( s1 ) ; s1 .colisionarCon ( s2 ) ; s1 .colisionarCon ( a1 ) ; }
La biblioteca YOMM2 [21] proporciona una implementación rápida y ortogonal de multimétodos abiertos.
La sintaxis para declarar métodos abiertos está inspirada en una propuesta de implementación nativa de C++. La biblioteca requiere que el usuario registre todas las clases utilizadas como argumentos virtuales (y sus subclases), pero no requiere ninguna modificación del código existente. Los métodos se implementan como funciones C++ en línea ordinarias; se pueden sobrecargar y se pueden pasar mediante punteros. No hay límite en la cantidad de argumentos virtuales y se pueden mezclar arbitrariamente con argumentos no virtuales.
La biblioteca utiliza una combinación de técnicas (tablas de envío comprimidas, tabla hash de enteros sin colisiones) para implementar llamadas de método en tiempo constante, al tiempo que se mitiga el uso de memoria. Enviar una llamada a un método abierto con un único argumento virtual requiere solo entre un 15 y un 30 % más de tiempo que llamar a una función miembro virtual común, cuando se utiliza un compilador optimizador moderno.
El ejemplo de Asteroides se puede implementar de la siguiente manera:
#include <yorel/yomm2/palabrasclave.hpp> #include <memoria> clase Cosa { pública : virtual ~ Cosa () {} }; clase Asteroide : cosa pública { }; clase Nave espacial : cosa pública { }; register_classes ( Cosa , Nave espacial , Asteroide ); declarar_metodo ( void , colisionarCon , ( virtual_ < Cosa &> , virtual_ < Cosa &> )); define_method ( void , collideWith , ( Thing & left , Thing & right )) { // manejo de colisiones predeterminado } define_method ( void , collideWith , ( Asteroide y izquierda , Asteroide y derecha )) { // manejar la colisión asteroide-asteroide } define_method ( void , collideWith , ( Asteroide e izquierda , Nave espacial e derecha )) { // manejar la colisión asteroide-nave espacial } define_method ( void , collideWith , ( Nave espacial e izquierda , Asteroide y derecha )) { // manejar la colisión nave espacial-asteroide } define_method ( void , collideWith , ( Nave espacial & izquierda , Nave espacial & derecha )) { // manejar la colisión entre naves espaciales } int principal () { yorel :: yomm2 :: métodos_de_actualización (); std :: unique_ptr < Cosa > a1 ( std :: make_unique < Asteroide > ()), a2 ( std :: make_unique < Asteroide > ()); std :: unique_ptr < Cosa > s1 ( std :: make_unique < Nave espacial > ()), s2 ( std :: make_unique < Nave espacial > ()); // nota: tipos parcialmente borrados collideWith ( * a1 , * a2 ); // Colisión asteroide-asteroide collideWith ( * a1 , * s1 ); // Colisión asteroide-nave espacial collideWith ( * s1 , * a1 ); // Colisión nave espacial-asteroide collideWith ( * s1 , * s2 ); // Colisión nave espacial-nave espacial devuelve 0 ; }
Stroustrup menciona en The Design and Evolution of C++ que le gustaba el concepto de métodos múltiples y consideró implementarlo en C++, pero afirma que no ha podido encontrar una implementación de muestra eficiente (comparable a las funciones virtuales) ni resolver algunos posibles problemas de ambigüedad de tipos. Luego afirma que, si bien sería bueno tener la característica, se puede implementar de manera aproximada utilizando double dispatch o una tabla de búsqueda basada en tipos como se describe en el ejemplo de C/C++ anterior, por lo que es una característica de baja prioridad para futuras revisiones del lenguaje. [22]
A partir de 2021 [actualizar], al igual que muchos otros lenguajes de programación orientados a objetos, D solo admite de forma nativa el envío único. Sin embargo, es posible emular métodos múltiples abiertos como una función de biblioteca en D. La biblioteca openmethods [23] es un ejemplo.
// Declaración Matriz más ( virtual ! Matriz , virtual ! Matriz ); // La anulación de dos objetos DenseMatrix @method Matrix _plus ( DenseMatrix a , DenseMatrix b ) { const int nr = a . rows ; const int nc = a . cols ; assert ( a . nr == b . nr ); assert ( a . nc == b . nc ); auto result = new DenseMatrix ; result . nr = nr ; result . nc = nc ; result . elems . length = a . elems . length ; result . elems [] = a . elems [] + b . elems []; return result ; } // La anulación de dos objetos DiagonalMatrix @method Matrix _plus ( DiagonalMatrix a , DiagonalMatrix b ) { assert ( a . rows == b . rows ); double [] sum ; sum . length = a . elems . length ; sum [] = a . elems [] + b . elems []; return new DiagonalMatrix ( sum ); }
En un lenguaje con un solo despacho, como Java , se pueden emular varios despachos con múltiples niveles de despacho único:
interfaz Collideable { void collideWith ( final Collideable otro ); /* Estos métodos necesitarían nombres diferentes en un lenguaje sin sobrecarga de métodos. */ void collideWith ( final Asteroid asteroid ); void collideWith ( final Spaceship spaceship ); } clase Asteroid implementa Collideable { public void collideWith ( final Collideable other ) { // Llamar a collideWith en el otro objeto. other . collideWith ( this ); } public void collideWith ( final Asteroid asteroid ) { // Manejar la colisión asteroide-asteroide. } public void collideWith ( final Spaceship spaceship ) { // Manejar la colisión entre un asteroide y una nave espacial. } } clase Spaceship implementa Collideable { public void collideWith ( final Collideable other ) { // Llamar a collideWith en el otro objeto. other . collideWith ( this ); } public void collideWith ( final Asteroid asteroid ) { // Manejar la colisión entre la nave espacial y el asteroide. } public void collideWith ( final Spaceship spaceship ) { // Manejar la colisión entre naves espaciales. } }
instanceof
También se pueden utilizar comprobaciones de tiempo de ejecución en uno o ambos niveles.
El envío múltiple (la selección de una función a invocar en función del tipo dinámico de dos o más argumentos) es una solución a varios problemas clásicos en la programación orientada a objetos.