En informática , el despacho dinámico es el proceso de seleccionar qué implementación de una operación polimórfica ( método o función) llamar en tiempo de ejecución . Se emplea comúnmente y se considera una característica principal de los lenguajes y sistemas de programación orientada a objetos (POO). [1]
Los sistemas orientados a objetos modelan un problema como un conjunto de objetos que interactúan y realizan operaciones a las que se hace referencia por su nombre. El polimorfismo es el fenómeno en el que objetos algo intercambiables exponen cada uno una operación con el mismo nombre pero posiblemente con un comportamiento diferente. Por ejemplo, un objeto Archivo y un objeto Base de datos tienen un método StoreRecord que se puede utilizar para escribir un registro de personal en el almacenamiento. Sus implementaciones difieren. Un programa contiene una referencia a un objeto que puede ser un objeto Archivo o un objeto Base de datos . Es posible que haya sido determinado por una configuración de tiempo de ejecución y, en esta etapa, es posible que el programa no sepa o no le importe cuál. Cuando el programa llama a StoreRecord en el objeto, algo debe elegir qué comportamiento se representa. Si uno piensa que la programación orientada a objetos envía mensajes a objetos, entonces en este ejemplo el programa envía un mensaje StoreRecord a un objeto de tipo desconocido, dejando que el sistema de soporte en tiempo de ejecución envíe el mensaje al objeto correcto. El objeto representa cualquier comportamiento que implemente. [2]
El envío dinámico contrasta con el envío estático , en el que la implementación de una operación polimórfica se selecciona en el momento de la compilación . El propósito del envío dinámico es diferir la selección de una implementación adecuada hasta que se conozca el tipo de tiempo de ejecución de un parámetro (o múltiples parámetros).
El envío dinámico es diferente del enlace tardío (también conocido como enlace dinámico). La vinculación de nombres asocia un nombre con una operación. Una operación polimórfica tiene varias implementaciones, todas asociadas con el mismo nombre. Los enlaces se pueden realizar en tiempo de compilación o (con enlace tardío) en tiempo de ejecución. Con el despacho dinámico, se elige una implementación particular de una operación en tiempo de ejecución. Si bien el envío dinámico no implica un enlace tardío, el enlace tardío sí implica un envío dinámico, ya que la implementación de una operación de enlace tardío no se conoce hasta el tiempo de ejecución. [ cita necesaria ]
La elección de qué versión de un método llamar puede basarse en un solo objeto o en una combinación de objetos. El primero se llama despacho único y es compatible directamente con lenguajes comunes orientados a objetos como Smalltalk , C++ , Java , C# , Objective-C , Swift , JavaScript y Python . En estos y lenguajes similares, uno puede llamar a un método de división con una sintaxis que se asemeja
dividendo . dividir ( divisor ) # dividendo / divisor
donde los parámetros son opcionales. Esto se considera como enviar un mensaje llamado divide con el parámetro divisor al dividendo . Se elegirá una implementación basándose únicamente en el tipo de dividendo (quizás racional , punto flotante , matriz ), sin tener en cuenta el tipo o valor del divisor .
Por el contrario, algunos lenguajes envían métodos o funciones basándose en la combinación de operandos; en el caso de la división, los tipos de dividendo y divisor juntos determinan qué operación de división se realizará. Esto se conoce como envío múltiple . Ejemplos de lenguajes que admiten envío múltiple son Common Lisp , Dylan y Julia .
Un lenguaje puede implementarse con diferentes mecanismos de despacho dinámico. Las opciones del mecanismo de despacho dinámico que ofrece un lenguaje alteran en gran medida los paradigmas de programación que están disponibles o que son más naturales de usar dentro de un lenguaje determinado.
Normalmente, en un lenguaje mecanografiado, el mecanismo de envío se realizará en función del tipo de argumentos (más comúnmente en función del tipo de receptor de un mensaje). Los idiomas con sistemas de tipificación débiles o nulos a menudo incluyen una tabla de despacho como parte de los datos de cada objeto. Esto permite el comportamiento de la instancia , ya que cada instancia puede asignar un mensaje determinado a un método separado.
Algunos idiomas ofrecen un enfoque híbrido.
El envío dinámico siempre generará una sobrecarga, por lo que algunos idiomas ofrecen envío estático para métodos particulares.
C++ utiliza enlace anticipado y ofrece distribución tanto dinámica como estática. La forma de envío predeterminada es estática. Para obtener un envío dinámico, el programador debe declarar un método como virtual .
Los compiladores de C++ normalmente implementan el envío dinámico con una estructura de datos llamada tabla de funciones virtuales (vtable) que define la asignación de nombre a implementación para una clase determinada como un conjunto de punteros de funciones miembro. Esto es puramente un detalle de implementación, ya que la especificación de C++ no menciona vtables. Las instancias de ese tipo almacenarán un puntero a esta tabla como parte de sus datos de instancia, lo que complica los escenarios cuando se utiliza la herencia múltiple . Dado que C++ no admite el enlace tardío, la tabla virtual de un objeto C++ no se puede modificar en tiempo de ejecución, lo que limita el conjunto potencial de objetivos de envío a un conjunto finito elegido en tiempo de compilación.
La sobrecarga de tipos no produce un envío dinámico en C++ ya que el lenguaje considera que los tipos de parámetros del mensaje forman parte del nombre formal del mensaje. Esto significa que el nombre del mensaje que ve el programador no es el nombre formal utilizado para el enlace.
En Go , Rust y Nim , se utiliza una variación más versátil de vinculación temprana. Los punteros de Vtable se transportan con referencias a objetos como 'punteros gruesos' ('interfaces' en Go u 'objetos de rasgo' en Rust [3] [4] ).
Esto desacopla las interfaces admitidas de las estructuras de datos subyacentes. Cada biblioteca compilada no necesita conocer la gama completa de interfaces admitidas para poder usar correctamente un tipo, solo el diseño de vtable específico que requieren. El código puede pasar diferentes interfaces al mismo dato para diferentes funciones. Esta versatilidad se produce a expensas de datos adicionales con cada referencia de objeto, lo que resulta problemático si muchas de esas referencias se almacenan de forma persistente.
El término puntero gordo simplemente se refiere a un puntero con información adicional asociada. La información adicional puede ser un puntero vtable para el envío dinámico descrito anteriormente, pero más comúnmente es el tamaño del objeto asociado para describir, por ejemplo, un segmento . [ cita necesaria ]
Smalltalk utiliza un despachador de mensajes basado en tipos. Cada instancia tiene un único tipo cuya definición contiene los métodos. Cuando una instancia recibe un mensaje, el despachador busca el método correspondiente en el mapa de mensaje a método para el tipo y luego invoca el método.
Debido a que un tipo puede tener una cadena de tipos base, esta búsqueda puede resultar costosa. Una implementación ingenua del mecanismo de Smalltalk parecería tener una sobrecarga significativamente mayor que la de C++ y esta sobrecarga se produciría por cada mensaje que reciba un objeto.
Las implementaciones reales de Smalltalk a menudo utilizan una técnica conocida como almacenamiento en caché en línea [5] que hace que el envío de métodos sea muy rápido. El almacenamiento en caché en línea básicamente almacena la dirección del método de destino anterior y la clase de objeto del sitio de llamada (o varios pares para el almacenamiento en caché multidireccional). El método almacenado en caché se inicializa con el método de destino más común (o simplemente con el controlador de errores de caché), según el selector de métodos. Cuando se llega al sitio de llamada del método durante la ejecución, simplemente llama a la dirección en el caché. (En un generador de código dinámico, esta llamada es una llamada directa ya que la dirección directa es reparada por la lógica de pérdida de caché). El código de prólogo en el método llamado luego compara la clase almacenada en caché con la clase de objeto real, y si no coinciden , la ejecución se bifurca a un controlador de errores de caché para encontrar el método correcto en la clase. Una implementación rápida puede tener múltiples entradas de caché y, a menudo, solo se necesitan un par de instrucciones para ejecutar el método correcto en un error de caché inicial. El caso común será una coincidencia de clase almacenada en caché y la ejecución simplemente continuará en el método.
El almacenamiento en caché fuera de línea también se puede utilizar en la lógica de invocación de métodos, utilizando la clase de objeto y el selector de métodos. En un diseño, el selector de clase y método tiene un hash y se utiliza como índice en una tabla de caché de distribución de métodos.
Como Smalltalk es un lenguaje reflexivo, muchas implementaciones permiten transformar objetos individuales en objetos con tablas de búsqueda de métodos generadas dinámicamente. Esto permite alterar el comportamiento de los objetos por objeto. A partir de esto ha surgido toda una categoría de lenguajes conocidos como lenguajes basados en prototipos , los más famosos de los cuales son Self y JavaScript . El diseño cuidadoso del almacenamiento en caché de envío de métodos permite que incluso los lenguajes basados en prototipos tengan un envío de métodos de alto rendimiento.
Muchos otros lenguajes de tipado dinámico, incluidos Python , Ruby , Objective-C y Groovy, utilizan enfoques similares.
clase Gato : def hablar ( auto ): imprimir ( "Miau" )clase Perro : def hablar ( self ): imprimir ( "Guau" )def hablar ( mascota ): # Envía dinámicamente el método hablar # mascota puede ser una instancia de Gato o Perro mascota . hablar ()gato = Gato () habla ( gato ) perro = Perro () habla ( perro )
#incluir <iostream> // convierte a Pet en una clase base virtual abstracta class Pet { public : virtual void talk () = 0 ; }; clase Perro : mascota pública { público : void hablar () anular { std :: cout << "¡Guau! \n " ; } }; clase Gato : mascota pública { pública : void hablar () anular { std :: cout << "¡Miau! \n " ; } }; // hablar() podrá aceptar cualquier cosa derivada de Mascota void hablar ( Mascota & mascota ) { mascota . hablar (); } int main () { Perro fido ; Gato simba ; hablar ( fido ); hablar ( simba ); devolver 0 ; }
Los objetos de rasgo realizan un despacho dinámico […] Cuando usamos objetos de rasgo, Rust debe usar el despacho dinámico. El compilador no conoce todos los tipos que podrían usarse con el código que usa objetos de rasgo, por lo que no sabe qué método implementado en qué tipo llamar. En cambio, en tiempo de ejecución, Rust usa los punteros dentro del objeto de rasgo para saber a qué método llamar. Esta búsqueda genera un costo de tiempo de ejecución que no ocurre con el envío estático. El envío dinámico también evita que el compilador elija incorporar el código de un método, lo que a su vez evita algunas optimizaciones.(xxix+1+527+3 páginas)
[…] La razón por la que Geos necesita 16 interrupciones es porque el esquema se utiliza para convertir llamadas a funciones entre segmentos ("lejos") en interrupciones, sin cambiar el tamaño del código. La razón por la que esto se hace es para que "algo" (el núcleo) pueda conectarse a cada llamada entre segmentos realizada por una aplicación Geos y asegurarse de que los segmentos de código adecuados se carguen desde la memoria virtual y se bloqueen. En términos de DOS , esto sería comparable a un cargador de superposición , pero se puede agregar sin requerir soporte explícito por parte del compilador o la aplicación. Lo que sucede es algo como esto: […] 1. El compilador en modo real genera una instrucción como esta: CALL <segment>:<offset> -> 9A <offlow><offhigh><seglow><seghigh> with <seglow>< seghigh> normalmente se define como una dirección que debe corregirse en el momento de la carga dependiendo de la dirección donde se haya colocado el código. […] 2. El enlazador Geos convierte esto en otra cosa: INT 8xh -> CD 8x […] DB <seghigh>,<offlow>,<offhigh> […] Tenga en cuenta que nuevamente son cinco bytes, por lo que puede ser fijado "en su lugar". Ahora el problema es que una interrupción requiere dos bytes, mientras que una instrucción CALL FAR solo necesita uno. Como resultado, el vector de 32 bits (<seg><ofs>) debe comprimirse en 24 bits. […] Esto se logra mediante dos cosas: primero, la dirección <seg> se codifica como un "identificador" del segmento, cuyo cuarteto más bajo es siempre cero. Esto ahorra cuatro bits. Además […] los cuatro bits restantes van al cuarteto bajo del vector de interrupción, creando así cualquier valor desde INT 80h hasta 8Fh. […] El controlador de interrupciones para todos esos vectores es el mismo. "Descomprimirá" la dirección de la notación de tres bytes y medio, buscará la dirección absoluta del segmento y reenviará la llamada, después de haber hecho la carga de la memoria virtual... El regreso de la llamada también pasar por el código de desbloqueo correspondiente. […] El mordisco bajo del vector de interrupción (80h–8Fh) contiene los bits 4 a 7 del identificador del segmento. Los bits 0 a 3 de un identificador de segmento son (por definición de identificador de Geos) siempre 0. […] todas las API de Geos se ejecutan mediante el esquema de "superposición" […]: cuando una aplicación Geos se carga en la memoria, el cargador automáticamente reemplace las llamadas a funciones en las bibliotecas del sistema por las correspondientes llamadas basadas en INT. De todos modos, estos no son constantes, sino que dependen del identificador asignado al segmento de código de la biblioteca.[…] Originalmente, Geos estaba destinado a convertirse al modo protegido desde el principio […], con el modo realsiendo sólo una "opción heredada" […] casi todas las líneas de código ensamblador están listas para ello […]
[…] en caso de punteros tan destrozados […] hace muchos años, Axel y yo estábamos pensando en una manera de usar *un* punto de entrada en un controlador para múltiples vectores de interrupción (ya que esto nos ahorraría mucho espacio para el múltiples puntos de entrada y el código de entramado de inicio/salida más o menos idéntico en todos ellos), y luego cambiar a los diferentes manejadores de interrupciones internamente. Por ejemplo: 1234h:0000h […] 1233h:0010h […] 1232h:0020h […] 1231h:0030h […] 1230h:0040h […] todos apuntan exactamente al mismo punto de entrada. Si conecta INT 21h a 1234h:0000h e INT 2Fh a 1233h:0010h, y así sucesivamente, todos pasarían por el mismo "vacío legal", pero aún podría distinguirlos y bifurcarse en los diferentes controladores internamente. Piense en un punto de entrada "comprimido" en un trozo A20 para la carga de HMA . Esto funciona siempre que ningún programa comience a realizar segmentos: magia de compensación. […] Compare esto con el enfoque opuesto de tener múltiples puntos de entrada (tal vez incluso admitiendo el Interrupt Sharing Protocol de IBM ), que consume mucha más memoria si conecta muchas interrupciones. […] Llegamos al resultado de que lo más probable es que esto no fuera seguro en la práctica porque nunca se sabe si otros conductores normalizan o desnormalizan los punteros, y por qué motivos. […](NB. Algo similar a los " punteros gordos " específicamente para el segmento de modo real de Intel : direccionamiento compensado en procesadores x86 , que contiene un puntero deliberadamente desnormalizado a un punto de entrada de código compartido y algo de información para seguir distinguiendo a las diferentes personas que llaman en el código compartido. Si bien, en un sistema abierto , las instancias de terceros que normalizan punteros (en otros controladores o aplicaciones) no se pueden descartar por completo en interfaces públicas , el esquema se puede utilizar de forma segura en interfaces internas para evitar secuencias de código de entrada redundantes).