stringtranslate.com

Despacho dinámico

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 pensamos en la POO como enviar 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 ]

Despacho único y múltiple

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 .

Mecanismos de despacho dinámico

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.

Implementación de C++

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.

Implementación de Go, Rust y Nim

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 ]

Implementación de Smalltalk

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 para todos y cada uno de los mensajes que recibe 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.

Ejemplo en Python

clase  Gato :  def  hablar ( yo ):  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 )

Ejemplo en C++

#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 ; }         

Ver también

Referencias

  1. ^ Milton, Scott; Schmidt, Heinz W. (1994). Despacho dinámico en lenguajes orientados a objetos (Informe técnico). vol. TR-CS-94-02. Universidad Nacional de Australia. CiteSeerX  10.1.1.33.4292 .
  2. ^ Driesen, Karel; Hölzle, Urs; Vitek, enero (1995). "Envío de mensajes en procesadores canalizados". ECOOP'95 — Programación orientada a objetos, novena conferencia europea, Åarhus, Dinamarca, 7 al 11 de agosto de 1995 . Apuntes de conferencias sobre informática. vol. 952. Saltador. CiteSeerX 10.1.1.122.281 . doi :10.1007/3-540-49538-X_13. ISBN  3-540-49538-X.
  3. ^ Klabnik, Steve; Nichols, Carol (2023) [2018]. "17. Funciones de programación orientada a objetos". El lenguaje de programación Rust (2 ed.). San Francisco, California, EE.UU.: No Starch Press, Inc. págs. 375–396 [379–384]. ISBN 978-1-7185-0310-6. pag. 384: 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)
  4. ^ "Objetos rasgo". La referencia del óxido . Consultado el 27 de abril de 2023 .
  5. ^ Müller, Martín (1995). Envío de mensajes en lenguajes orientados a objetos de tipo dinámico (tesis de maestría). Universidad de Nuevo México. págs. 16-17. CiteSeerX 10.1.1.55.1782 . 

Otras lecturas