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 distribuir 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 el 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 suelen organizar el código fuente en bloques con nombres, denominados subrutinas , procedimientos, subprogramas, funciones o métodos. El código de la función se ejecuta llamándola , ejecutando un fragmento de código que hace referencia a su nombre . Esto transfiere el control temporalmente a la función llamada; cuando se completa la ejecución de la función, el control generalmente se transfiere de nuevo a la instrucción del llamador que sigue a la referencia.
Los nombres de las funciones generalmente se seleccionan de manera que describan el propósito de la función. A veces es deseable dar el mismo nombre a varias funciones, 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 a la función no es suficiente para identificar el bloque de código que se ejecutará. En cambio, el número y el tipo de argumentos de la llamada a la función también se utilizan para seleccionar entre varias implementaciones de funciones.
En lenguajes de programación orientados a objetos más convencionales, es decir, de envío único , al invocar 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 usa para determinar cuál de los (potencialmente) muchas) clases de métodos con ese nombre se van a aplicar. En muchos idiomas, el argumento especial se indica sintácticamente; por ejemplo, varios lenguajes de programación ponen 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.
Por el contrario, en lenguajes con despacho múltiple, el método seleccionado es simplemente aquel cuyos argumentos coinciden con el número y tipo de llamada a la función. No existe ningún argumento especial que sea propietario de la función/método llevado a cabo en una llamada en particular.
El Common Lisp Object System (CLOS) es un ejemplo temprano y bien conocido de envío 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 la información de escritura estática, como el tipo declarado o inferido de un término (o el tipo base en un lenguaje con subtipos) se usa para determinar cuál de varias posibilidades se usará en un sitio de llamada determinado. y esa determinación se realiza en el momento de la compilación o del enlace (o en algún otro momento antes de que comience la ejecución del programa) y posteriormente es invariante para una implementación o ejecución determinada del programa. Muchos lenguajes como C++ ofrecen una sobrecarga de funciones sólida, pero no ofrecen 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 el momento de la compilación , se puede seleccionar entre las alternativas. El acto de crear funciones alternativas para la selección en tiempo de compilación generalmente se denomina sobrecarga de una función.
En los lenguajes de programación que difieren la identificación del tipo de datos hasta el tiempo de ejecución (es decir, enlace tardío ), la selección entre funciones alternativas debe ocurrir en ese momento, 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 generalmente multimétodos .
Existe cierto costo de tiempo de ejecución asociado con el envío dinámico de llamadas a funciones. En algunos idiomas, [ cita necesaria ] la distinción entre sobrecarga y métodos múltiples puede ser borrosa, y el compilador determina si la selección del tiempo de compilación se puede aplicar a una llamada de función determinada o si se necesita un tiempo de ejecución más lento.
Hay varios problemas conocidos con el envío dinámico, tanto únicos como múltiples. Si bien muchos de estos problemas se resuelven con el envío único, 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 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 idioma. 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 todo el programa 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 para 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 OO 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 envío múltiple, si se implementa correctamente, permite ambos. Es deseable que una implementación de despacho múltiple tenga las siguientes propiedades:
Generalmente es deseable que para cualquier invocación dada de un método múltiple, haya como máximo un "mejor" candidato entre los casos de implementación del método múltiple, y/o que si no lo hay, esto se resuelva de una manera predecible y manera determinista, incluido 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 despacho único y en ausencia de herencia múltiple , esta condición se cumple trivialmente, pero con despacho 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 siendo el subtipo en un caso, otro siendo el subtipo en el otro caso). Esto puede suceder particularmente si dos paquetes diferentes, que no dependen 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:
Implementación eficiente de envío único, incluso en lenguajes de programación que se compilan por separado en código objeto y se vinculan con un vinculador de bajo nivel (que no reconoce el idioma), incluso dinámicamente en el momento de carga/inicio del programa o incluso bajo la dirección de la aplicación. código, son bien conocidos. 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 a un método estático, requiriendo una sobrecarga O(1) y sólo una llamada adicional. Búsqueda de memoria incluso en el caso no optimizado. Sin embargo, el método vtable utiliza el nombre de la función y no el tipo de argumento como clave de búsqueda, y no se escala al caso de envío múltiple. (También depende del paradigma orientado a objetos de que los métodos sean características de clases, no entidades independientes independientes de cualquier tipo de datos en particular).
La implementación eficiente del envío múltiple sigue siendo un problema de investigación en curso.
Para estimar con qué frecuencia se utiliza el envío múltiple en la práctica, Muschevici et al. [2] estudiaron programas que utilizan despacho dinámico. Analizaron nueve aplicaciones, en su mayoría compiladores, escritas 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 (anulación) 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 funciones con anulaciones más concretas.
El envío múltiple se usa mucho más en Julia , donde el envío múltiple era un concepto de diseño central desde el origen del lenguaje: al recopilar las mismas estadísticas que Muschevici sobre el número promedio de métodos por función genérica, se descubrió que la biblioteca estándar de Julia usa más del doble 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 el índice de despacho DR
es el número promedio de métodos por función genérica; el índice de elección CR
es la media del cuadrado del número de métodos (para medir mejor la frecuencia de funciones con un gran número 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 múltiples lenguajes de despacho 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 los lenguajes orientados a objetos [6] y una solución al problema de los métodos binarios. [7]
La distinción entre envíos múltiples y únicos puede aclararse con un ejemplo. Imagine un juego que tenga, entre sus objetos (visibles para el usuario), naves espaciales y asteroides. Cuando dos objetos chocan, es posible que el programa deba hacer cosas diferentes según lo que acaba de golpear.
C# introdujo soporte para multimétodos dinámicos en la versión 4 [8] (abril de 2010) utilizando la palabra clave 'dinámica'. El siguiente ejemplo demuestra múltiples métodos. Como 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 la escritura estática en lugar de la escritura dinámica en la mayoría de los escenarios. [10] La palabra clave 'dinámica' admite la interoperabilidad con objetos COM y lenguajes .NET de tipo dinámico.
El siguiente ejemplo utiliza características introducidas en C# 9 y C# 10.
usando ColliderLibrary estático ; Consola . WriteLine ( Collide ( nuevo asteroide ( 101 ), nueva nave espacial ( 300 ))); Consola . WriteLine ( Collide ( nuevo asteroide ( 10 ), nueva nave espacial ( 10 ))); Consola . WriteLine ( Collide ( nueva nave espacial ( 101 ), nueva nave espacial ( 10 ))); cadena Chocar ( SpaceObject x , SpaceObject y ) => x . Talla > 100 && y . Tamaño > 100 ? "¡Gran boom!" : CollideWith ( x como dinámico , y como dinámico ); // Envío dinámico al método CollideWith class 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 ); registrar Asteroide ( int Tamaño ) : SpaceObject ( Tamaño ); registrar nave espacial ( int tamaño ) : objeto espacial ( tamaño );
Producción:
¡Gran auge! a/s s/s
Groovy es un lenguaje JVM interutilizable/compatible 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 igual cuando se usan métodos no estáticos o se compilan clases/métodos estáticamente (@CompileStatic annotation) */ class Program { static void main ( String [] args ) { println Collider . colisionar ( nuevo asteroide ( 101 ), nueva nave espacial ( 300 )) println Collider . colisionar ( nuevo asteroide ( 10 ), nueva nave espacial ( 10 )) println Collider . colisionar ( nueva nave espacial ( 101 ), nueva nave espacial ( 10 )) } } clase Collider { cadena estática colisionar ( SpaceObject x , SpaceObject y ) { ( x . tamaño > 100 && y . tamaño > 100 ) ? "big-boom" : colisionar con ( x , y ) // Envío dinámico al método colisionar con } cadena estática privada choca con ( asteroide x , asteroide y ) { "a/a" } cadena estática privada choca con ( asteroide x , nave espacial y ) { "a/s" } cadena estática privada choca con ( nave espacial x , asteroide y ) { "s /a" } Cadena estática privada colisiona con ( 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 distribución múltiple, como Common Lisp , podría parecerse más a esto (se muestra un ejemplo de Common Lisp):
( defmethod choca con (( x asteroide ) ( y asteroide )) ;; lidiar con un asteroide que choca con un asteroide ) ( defmethod choca con (( x asteroide ) ( y nave espacial )) ;; lidiar con un asteroide que golpea una nave espacial ) ( defmethod choca con with (( x nave espacial ) ( y asteroide )) ;; lidiar con una nave espacial que choca contra un asteroide ) ( defmethod colisión-con (( x nave espacial ) ( y nave espacial )) ;; lidiar con una nave espacial que golpea una nave espacial )
y lo mismo para los otros métodos. No se utilizan pruebas explícitas ni "transmisión dinámica".
En presencia de despacho múltiple, 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 no se agrupan en clases sino en funciones genéricas .
Julia tiene despacho múltiple incorporado y es fundamental para el diseño del lenguaje. [3] La versión de Julia del ejemplo anterior podría verse así:
tipo abstracto final SpaceObject estructura Asteroide <: tamaño del objeto espacial :: fin int estructura nave espacial <: tamaño del objeto espacial :: fin int 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 :: SpaceObject , y :: SpaceObject ) = ( x . tamaño > 100 && y . tamaño > 100 ) ? "¡Gran boom!" : colisionar_con ( x , y )
Producción:
julia> colisionar ( Asteroide ( 101 ), Nave espacial ( 300 )) "¡Gran boom!" 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 poderosa semántica del lado del usuario a través de envío múltiple.
Tiene tanto multimétodos como multisubs. Dado que la mayoría de los operadores son subrutinas, también tiene múltiples operadores enviados.
Junto con las restricciones de tipo habituales, también tiene restricciones donde que permiten realizar subrutinas muy especializadas.
subconjunto Masa de Real donde 0 ^..^ Inf ; rol Objeto estelar { tiene masa $. Se requiere masa ; nombre del método () devuelve Str {...}; }clase Asteroide hace Objeto Estelar { nombre del método () { 'un asteroide' }}clase Nave espacial hace Objeto estelar { tiene Str $.name = 'alguna nave espacial sin nombre' ;}my Str @destroyed = < borrado destruido destrozado > ; my Str @damaged = « dañado 'chocó con' 'fue dañado por' »;# Agregamos múltiples candidatos a los operadores de comparación numérica porque los estamos comparando numéricamente, # pero no tiene sentido forzar los objetos a un tipo numérico. # (Si lo hicieran, no necesariamente necesitaríamos agregar estos operadores). # También podríamos haber definido operadores completamente nuevos de la misma manera. multi sub infijo: « <=> » ( Objeto estelar:D $a , Objeto estelar:D $b ) { $a . masa <=> $b . masa } multi sub infijo: « < » ( Objeto estelar:D $a , Objeto estelar:D $b ) { $a . masa < $b . masa } multi sub infijo: « > » ( Objeto-estelar:D $a , Objeto-estelar:D $b ) { $a . masa > $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 tuviera restricciones. proto sub colisión ( Objeto estelar:D $, Objeto estelar:D $ ) {*}# No es necesario repetir los tipos aquí ya que son los mismos que los del prototipo. # La restricción 'dónde' técnicamente solo se aplica a $b, no a toda la firma. # Tenga en cuenta que la restricción 'dónde' utiliza el candidato a operador `<` que agregamos anteriormente. colisión múltiple ( $a , $b donde $a < $b ) { diga "$a.name() fue @destroyed.pick ( ) por $b.name()" ; }multi sub colision ( $a , $b donde $a > $b ) { # redispacho al candidato anterior con los argumentos intercambiados igual con $b , $a ;}# Esto tiene que ser después de los dos primeros porque los otros # tienen restricciones de 'dónde', que se verifican en el # orden en que se escribieron los subs. (Este siempre coincidiría. ) multi sub colisión ( $a , $b ) { # aleatorizar el orden my ( $n1 , $n2 ) = ( $a . nombre , $b . nombre ). elegir (*); diga "$n1 @dañado.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 los barcos tienen masas desiguales, se llama a uno de los dos primeros candidatos. colisión múltiple ( Nave espacial $a , Nave espacial $b donde $a == $b ){ my ( $n1 , $n2 ) = ( $a . nombre , $b . nombre ). elegir (*); diga "$n1 chocó con $n2, y ambos barcos fueron" , ( @destroyed . pick , 'dejado dañado' ). elegir ;}# Puedes descomprimir los atributos en variables dentro de la firma. # Incluso podrías tener una restricción sobre ellos `(:mass($a) donde 10)`. colisión múltiple ( Asteroide $ ( : masa ( $a )), Asteroide $ (: masa ( $b ))){ diga "dos asteroides chocaron y se combinaron en un asteroide más grande de masa { $a + $b }" ; }mi Nave Espacial $Empresa .= new (: masa ( 1 ),: nombre ( 'La Empresa' )); colisionan con un asteroide . nuevo (: masa ( .1 )), $Empresa ; colisionan $Enterprise , Nave espacial . nuevo (: masa ( .1 )); colisionan $Enterprise , Asteroide . nuevo (: masa ( 1 )); colisionan $Enterprise , Nave espacial . nuevo (: masa ( 1 )); colisionan con un asteroide . nuevo (: masa ( 10 )), Asteroide . nuevo (: masa ( 5 ));
En los idiomas que no admiten el envío múltiple a nivel de definición de idioma 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 envíos múltiples a través de una biblioteca. Por ejemplo, el paquete multimétodo [12] proporciona una implementación de funciones genéricas de despacho múltiple.
Versión escrita dinámicamente en JavaScript:
importar { multi , método } desde '@arrows/multimethod' clase Asteroide {} clase Nave espacial {} const collideWith = multi ( método ([ Asteroide , Asteroide ], ( x , y ) => { // tratar con un asteroide que golpea un asteroide }), método ([ Asteroide , Nave espacial ], ( x , y ) => { // tratar con un asteroide golpeando una nave espacial }), método ([ Nave espacial , Asteroide ], ( x , y ) => { // lidiar con una nave espacial golpeando un asteroide }), método ([ Nave espacial , Nave espacial ], ( x , y ) => { / / lidiar con una nave espacial que choca contra una nave espacial }), )
Versión escrita estáticamente en TypeScript:
importar { multi , método , Multi } desde '@arrows/multimethod' clase Asteroide {} clase Nave espacial {} escriba CollideWith = Multi & { ( x : Asteroide , y : Asteroide ) : vacío ( x : Asteroide , y : Nave espacial ) : vacío ( x : Nave espacial , y : Asteroide ) : vacío ( x : Nave espacial , y : Nave espacial ) : vacío } const colisionar con : CollideWith = multi ( método ([ Asteroide , Asteroide ], ( x , y ) => { // tratar con un asteroide que golpea un asteroide }), método ([ Asteroide , Nave espacial ], ( x , y ) => { / / lidiar con un asteroide que golpea una nave espacial }), método ([ Nave espacial , Asteroide ], ( x , y ) => { // lidiar con una nave espacial que golpea un asteroide }), método ([ Nave espacial , Nave espacial ], ( x , y ) => { // lidiar con una nave espacial que choca contra una nave espacial }), )
Se pueden agregar envíos múltiples 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 multimétodos estilo CLOS para Python sin cambiar la sintaxis subyacente o las palabras clave del lenguaje.
desde multimétodos importar Envío desde game_objects importar asteroide , nave espacial desde game_behaviors importar as_func , ss_func , sa_funccolisionar = Despacho () colisionar . add_rule (( asteroide , nave espacial ), as_func ) colisionan . add_rule (( nave espacial , nave espacial ), ss_func ) colisionan . add_rule (( nave espacial , asteroide ), sa_func )def aa_func ( a , b ): """Comportamiento cuando un asteroide choca contra un asteroide.""" # ...definir un nuevo comportamiento... chocar . add_rule (( Asteroide , Asteroide ), aa_func )
# ...más tarde... colisionar ( cosa1 , cosa2 )
Funcionalmente, esto es muy similar al ejemplo CLOS, pero la sintaxis es la convencional de Python.
Utilizando decoradores de Python 2.4 , Guido van Rossum produjo una implementación de muestra de multimétodos [15] con una sintaxis simplificada:
@multimethod ( Asteroide , Asteroide ) def colisiona ( a , b ): """Comportamiento cuando un asteroide choca contra un asteroide.""" # ...define un nuevo comportamiento... @multimethod ( Asteroide , Nave espacial ) def colisiona ( a , b ): """Comportamiento cuando un asteroide choca contra una nave espacial.""" # ...definir nuevo comportamiento... # ... definir 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 el envío múltiple con una sintaxis aún más simple. Por ejemplo, usando plum-dispatch,
del despacho de importación de ciruela @dispatch def colisionar ( a : Asteroide , b : Asteroide ): """Comportamiento cuando un asteroide choca contra otro.""" # ...definir nuevo comportamiento... @dispatch def colisionar ( a : Asteroide , b : Nave espacial ): """Comportamiento cuando un asteroide golpea una nave espacial.""" # ...definir nuevo comportamiento... # ...definir reglas adicionales...
C no tiene despacho dinámico, por lo que debe implementarse manualmente de alguna forma. A menudo se utiliza una enumeración para identificar el subtipo de un objeto. El envío dinámico se puede realizar buscando este valor en una tabla de rama de puntero de función . Aquí hay un ejemplo simple en C:
typedef vacío ( * CollisionCase ) ( vacío ); void Collision_AA ( void ) { /* manejar la colisión asteroide-asteroide */ }; void Collision_AS ( void ) { /* manejar la colisión entre asteroides y naves espaciales */ }; void Collision_SA ( void ) { /* manejar la colisión nave espacial-asteroide */ }; void colisión_SS ( void ) { /* manejar la colisión nave espacial-nave espacial*/ }; typedef enum { THING_ASTEROID = 0 , THING_SPACESHIP , THING_COUNT /* no es un tipo de cosa en sí, sino que se usa para encontrar una cantidad de cosas */ } Cosa ; CollisionCase casos de colisión [ COSA_COUNT ][ COSA_COUNT ] = { { & colisión_AA , & colisión_AS }, { & colisión_SA , & colisión_SS } }; colisión de vacío ( Cosa a , Cosa b ) { ( * casos de colisión [ a ][ b ])(); } int main ( vacío ) { colisionar ( THING_SPACESHIP , THING_ASTEROID ); }
Con la biblioteca C Object System, [19] C admite el despacho 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:
#incluye <stdio.h> #incluye <cos/Object.h> #incluye <cos/gen/object.h> // clasesdefclass ( Asteroide ) // miembros de datos clase final defclass ( nave espacial ) // miembros de datos clase final // genéricosdefgeneric ( _Bool , colisionar_con , _1 , _2 ); // multimétodosdefmethod ( _Bool , collide_with , Asteroid , Asteroid ) // lidiar con el asteroide que choca con el asteroide endmethod defmethod ( _Bool , collide_with , asteroide , nave espacial ) // lidiar con el asteroide que golpea la nave espacial endmethod defmethod ( _Bool , colisionar_con , nave espacial , asteroide ) // tratar con una nave espacial que choca contra un asteroide endmethod defmethod ( _Bool , collide_with , Spaceship , Spaceship ) // lidiar con la nave espacial que golpea la nave espacial endmethod // ejemplo de usoint main ( vacío ) { OBJ a = gnew ( Asteroide ); OBJ s = gnew ( nave espacial ); printf ( "<a,a> = %d \n " , colisiona_con ( a , a )); printf ( "<a,s> = %d \n " , colisiona_con ( a , s )); printf ( "<s,a> = %d \n " , colisiona_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 el envío único, aunque Bjarne Stroustrup (y sus colaboradores) propusieron agregar métodos múltiples (despacho múltiple) en 2007. [20] Los métodos para solucionar este límite son análogos: use el patrón de visitante , reparto dinámico o una biblioteca:
// Ejemplo de comparación de tipos de tiempo de ejecución a través dedynamic_cast struct Thing { virtual void choca con ( Cosa y otros ) = 0 ; }; struct Asteroid : Thing { void collideWith ( Thing & other ) { // la transmisión dinámica a un tipo de puntero devuelve NULL si la conversión falla // (la transmisión dinámica a un tipo de referencia generaría una excepción en caso de falla) if ( auto asteroide = transmisión_dinámica < Asteroide * > ( y otros )) { // manejar la colisión asteroide-asteroide } else if ( auto spaceship = dynamic_cast < nave espacial *> ( y otros )) { // manejar la colisión asteroide-nave espacial } else { // manejo predeterminado de colisiones aquí } } }; estructura Nave espacial : Cosa { void colisiona con ( Cosa y otros ) { if ( auto asteroide = dynamic_cast < Asteroide *> ( y otros )) { // manejar la colisión nave espacial-asteroide } else if ( auto spaceship = dynamic_cast < Nave espacial *> ( & other )) { // manejar la colisión entre naves espaciales } else { // manejo predeterminado de colisiones aquí } } };
o tabla de búsqueda de puntero a método:
#include <cstdint> #include <typeinfo> #include <unordered_map> clase Cosa { protegida : Cosa ( std :: uint32_t cid ) : tid ( cid ) {} const std :: uint32_t tid ; // escriba identificación typedef void ( Cosa ::* CollisionHandler )( Cosa y otros ); typedef std :: unordered_map < std :: uint64_t , CollisionHandler > CollisionHandlerMap ; addHandler vacío estático ( std :: uint32_t id1 , std :: uint32_t id2 , controlador CollisionHandler ) { CollisionCases . insertar ( CollisionHandlerMap :: valor_tipo ( clave ( id1 , id2 ), controlador )); } static std :: uint64_t key ( std :: uint32_t id1 , std :: uint32_t id2 ) { return std :: uint64_t ( id1 ) << 32 | id2 ; } Casos de colisión estáticos CollisionHandlerMap ; public : void collideWith ( Cosa y otra ) { auto handler = colisionCases . buscar ( clave ( tid , otro . tid )); if ( controlador ! = casosdecolisión . fin ()) { ( este ->* controlador -> segundo ) ( otro ); // llamada de puntero a método } else { // manejo de colisiones predeterminado } } }; clase Asteroide : Cosa pública { void asteroid_collision ( Cosa y otros ) { /*manejar la colisión asteroide-asteroide*/ } void spaceship_collision ( Cosa y otros ) { /*manejar la colisión asteroide-nave espacial*/ } público : Asteroide () : Cosa ( cid ) {} initCases estático vacío (); estática constante std :: uint32_t cid ; }; clase Nave espacial : Cosa pública { 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 (); estática constante std :: uint32_t cid ; // identificador de clase }; Cosa :: CollisionHandlerMap Cosa :: colisionCases ; const std :: uint32_t Asteroide :: cid = typeid ( Asteroide ). código hash (); const std :: uint32_t Nave espacial :: cid = typeid ( Nave espacial ). código hash (); void Asteroid::initCases () { addHandler ( cid , cid , CollisionHandler ( & Asteroid :: asteroid_collision )); addHandler ( cid , Nave espacial :: cid , CollisionHandler ( & Asteroide :: nave espacial_colisión )); } nave espacial vacía ::initCases () { addHandler ( cid , asteroide :: cid , CollisionHandler ( & nave espacial :: asteroid_collision )); addHandler ( cid , cid , CollisionHandler ( & nave espacial :: nave espacial_colisión )); } int main () { Asteroide :: initCases (); Nave espacial :: initCases (); Asteroide a1 , a2 ; Nave espacial s1 , s2 ; a1 . colisionar con ( a2 ); a1 . colisionar con ( s1 ); s1 . colisionar con ( s2 ); s1 . colisionar con ( a1 ); }
La biblioteca YOMM2 [21] proporciona una implementación ortogonal rápida de multimétodos abiertos.
La sintaxis para declarar métodos abiertos está inspirada en una propuesta para una 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 ordinarias de C++ en línea; se pueden sobrecargar y se pueden pasar por puntero. 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 distribución comprimidas, tabla hash de enteros sin colisiones) para implementar llamadas a métodos en tiempo constante, al tiempo que mitiga el uso de memoria. Enviar una llamada a un método abierto con un único argumento virtual lleva sólo entre un 15% y un 30% más de tiempo que llamar a una función miembro virtual ordinaria, cuando se utiliza un compilador de optimización moderno.
El ejemplo de Asteroides se puede implementar de la siguiente manera:
#include <yorel/yomm2/keywords.hpp> #include <memoria> clase Cosa { público : virtual ~ Cosa () {} }; clase Asteroide : cosa pública { }; clase Nave espacial : cosa pública { }; Register_classes ( Cosa , Nave Espacial , Asteroide ); declarar_metodo ( void , colisionar con , ( virtual_ < cosa &> , virtual_ < cosa &> )); define_method ( void , colisionar con , ( cosa e izquierda , cosa y derecha )) { // manejo de colisiones predeterminado } define_method ( void , colisionar con , ( asteroide e izquierda , asteroide y derecha )) { // manejar la colisión asteroide-asteroide } define_method ( void , colisionar con , ( asteroide e izquierda , nave espacial y derecha )) { // manejar la colisión asteroide-nave espacial } define_method ( void , colisionar con , ( nave espacial e izquierda , asteroide y derecha )) { // manejar la colisión nave espacial-asteroide } define_method ( void , colisionar con , ( nave espacial e izquierda , nave espacial y derecha )) { // manejar la colisión nave espacial-nave espacial } int main () { yorel :: yomm2 :: update_methods (); 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 colisionar con ( * a1 , * a2 ); // Colisión asteroide-asteroide colisiona con ( * a1 , * s1 ); // Colisión asteroide-nave espacial colisiona con ( * s1 , * a1 ); // Colisión nave espacial-asteroide colisiona con ( * s1 , * s2 ); // Colisión nave espacial-nave espacial devolver 0 ; }
Stroustrup menciona en The Design and Evolution of C++ que le gustó el concepto de multimétodos y consideró implementarlo en C++, pero afirma haber sido incapaz de encontrar una implementación de muestra eficiente (comparable a funciones virtuales) y resolver algunos posibles problemas de ambigüedad de tipos. Luego afirma que, aunque sería bueno tener la característica, se puede implementar aproximadamente usando despacho doble 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 admite de forma nativa solo el envío único. Sin embargo, es posible emular multimétodos abiertos como una función de biblioteca en D. La biblioteca openmethods [23] es un ejemplo.
// Declaración Matriz plus ( virtual ! Matriz , virtual ! Matriz ); // La anulación para dos objetos DenseMatrix @method Matrix _plus ( DenseMatrix a , DenseMatrix b ) { const int nr = a . filas ; constante int nc = a . columnas ; afirmar ( a . nr == b . nr ); afirmar ( a . nc == b . nc ); resultado automático = nueva DenseMatrix ; resultado . nr = nr ; resultado . nc = nc ; resultado . elementos . longitud = a . elementos . longitud ; resultado . elementos [] = a . elementos [] + b . elementos []; resultado de retorno ; } // La anulación para dos objetos DiagonalMatrix @method Matrix _plus ( DiagonalMatrix a , DiagonalMatrix b ) { afirmar ( a . filas == b . filas ); doble [] suma ; suma . longitud = a . elementos . longitud ; suma [] = a . elementos [] + b . elementos []; devolver nueva DiagonalMatrix ( suma ); }
En un lenguaje con un solo envío único, como Java , se puede emular el envío múltiple con múltiples niveles de envío único:
interfaz colisionable { void colisionar con ( final colisionable otro ); /* Estos métodos necesitarían nombres diferentes en un idioma sin sobrecarga de métodos. */ colisión de vacío con ( asteroide asteroide final ); colisión vacía con ( nave espacial final de la nave espacial ); } class Asteroid implements Collideable { public void collideWith ( final Collideable other ) { // Llama a collideWith en el otro objeto. otro . colisionar con ( esto ); } colisión de vacío públicoCon ( asteroide final asteroide ) { // Manejar la colisión asteroide-asteroide. } colisión de vacío públicoCon ( nave espacial final ) { // Manejar la colisión entre asteroide y nave espacial. } } class Spaceship implements Collideable { public void collideWith ( final Collideable other ) { // Llama a collideWith en el otro objeto. otro . colisionar con ( esto ); } colisión de vacío público con ( asteroide final ) { // Manejar la colisión entre nave espacial y asteroide. } public void colisiona con ( final nave espacial nave espacial ) { // Maneja la colisión nave espacial-nave espacial. } }
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 de la programación orientada a objetos.