stringtranslate.com

Despacho dinámico

En informática , el envío dinámico es el proceso de seleccionar qué implementación de una operación polimórfica ( método o función) se llamará en tiempo de ejecución . Se emplea comúnmente en lenguajes y sistemas de programación orientada a objetos (POO) y se considera una característica principal de ellos. [1]

Los sistemas orientados a objetos modelan un problema como un conjunto de objetos que interactúan y ejecutan operaciones a las que se hace referencia por su nombre. El polimorfismo es el fenómeno en el que objetos que son intercambiables exponen cada uno una operación con el mismo nombre pero que posiblemente difieren en su comportamiento. Por ejemplo, un objeto File y un objeto Database 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 File o un objeto Database . Es posible que una configuración de tiempo de ejecución haya determinado cuál de ellos es y, en esta etapa, el programa puede no saber o no importarle cuál. Cuando el programa llama a StoreRecord en el objeto, algo debe elegir qué comportamiento se ejecuta. Si pensamos en la programación orientada a objetos como el envío de 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 de tiempo de ejecución envíe el mensaje al objeto correcto. El objeto ejecuta el comportamiento que implementa. [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 tiempo de compilación . El propósito del envío dinámico es posponer 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). El enlace de nombre 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 envío 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 enlace tardío, el enlace tardío sí implica envío dinámico, ya que la implementación de una operación enlazada tardío no se conoce hasta el tiempo de ejecución. [ cita requerida ]

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 denomina envío único y está directamente respaldado por lenguajes orientados a objetos comunes como Smalltalk , C++ , Java , C# , Objective-C , Swift , JavaScript y Python . En estos y otros lenguajes similares, se puede llamar a un método para la división con una sintaxis que se parezca a

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 a dividend . Se elegirá una implementación basándose únicamente en el tipo de dividend (quizás racional , punto flotante , matriz ), sin tener en cuenta el tipo o valor de divisor .

Por el contrario, algunos lenguajes envían métodos o funciones en función de 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 el envío múltiple son Common Lisp , Dylan y Julia .

Mecanismos de despacho dinámico

Un lenguaje puede implementarse con distintos mecanismos de distribución dinámica. Las opciones del mecanismo de distribución dinámica que ofrece un lenguaje modifican en gran medida los paradigmas de programación disponibles o que son más naturales de usar dentro de un lenguaje determinado.

Normalmente, en un lenguaje tipado, 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 lenguajes con sistemas de tipado débiles o inexistentes suelen incluir una tabla de envío como parte de los datos de objeto para cada objeto. Esto permite el comportamiento de instancias , ya que cada instancia puede asignar un mensaje determinado a un método independiente.

Algunos idiomas ofrecen un enfoque híbrido.

El envío dinámico siempre implicará una sobrecarga, por lo que algunos lenguajes ofrecen envío estático para métodos particulares.

Implementación en C++

C++ utiliza la vinculación temprana y ofrece tanto envío dinámico como estático. La forma predeterminada de envío es estática. Para obtener un envío dinámico, el programador debe declarar un método como virtual .

Los compiladores de C++ suelen implementar el envío dinámico con una estructura de datos denominada 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 las 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 la vinculación tardía, la tabla virtual en un objeto de C++ no se puede modificar en tiempo de ejecución, lo que limita el conjunto potencial de destinos 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 los tipos de los parámetros del mensaje como parte del nombre formal del mensaje. Esto significa que el nombre del mensaje que ve el programador no es el nombre formal utilizado para la vinculación.

Implementación de Go, Rust y Nim

En Go , Rust y Nim , se utiliza una variación más versátil del enlace temprano. Los punteros Vtable se transportan con referencias de objetos como 'punteros fat' ('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 usar correctamente un tipo, solo el diseño de tabla virtual específico que requieren. El código puede pasar diferentes interfaces al mismo fragmento de datos a diferentes funciones. Esta versatilidad se produce a expensas de datos adicionales con cada referencia de objeto, lo que es 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 de tabla virtual para el envío dinámico descrito anteriormente, pero es más común que sea el tamaño del objeto asociado para describir, por ejemplo, una porción . [ cita requerida ]

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 mensajes a métodos 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 simple del mecanismo de Smalltalk parecería tener una sobrecarga significativamente mayor que la de C++ y esta sobrecarga se generaría por cada mensaje que reciba un objeto.

Las implementaciones reales de Smalltalk suelen utilizar 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 solo 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 la caché. (En un generador de código dinámico, esta llamada es una llamada directa ya que la dirección directa se parchea de nuevo mediante la lógica de errores 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 ramifica a un controlador de errores de caché para encontrar el método correcto en la clase. Una implementación rápida puede tener varias entradas de caché y, a menudo, solo se necesitan un par de instrucciones para obtener la ejecución en 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, la clase y el selector de métodos se codifican y se utilizan como índice en una tabla de caché de envío de métodos.

Como Smalltalk es un lenguaje reflexivo, muchas implementaciones permiten mutar objetos individuales en objetos con tablas de búsqueda de métodos generadas dinámicamente. Esto permite alterar el comportamiento de los objetos en función de cada objeto. A partir de esto, ha surgido toda una categoría de lenguajes conocidos como lenguajes basados ​​en prototipos , de los cuales los más famosos 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 tipados dinámicamente, incluidos Python , Ruby , Objective-C y Groovy, utilizan enfoques similares.

Ejemplo en Python

clase  Gato :  def  hablar ( self ):  imprimir ( "Miau" )clase  Perro :  def  hablar ( self ):  imprimir ( "Guau" )def  speak ( pet ):  # Despacha dinámicamente el método speak  # pet puede ser una instancia de Cat o Dog  pet . speak ()gato  =  Gato () habla ( gato ) perro  =  Perro () habla ( perro )

Ejemplo en C++

#include <flujo de datos> // hacer de Pet una clase base virtual abstracta class Pet { public : virtual void speak () = 0 ; };       clase Perro : público Mascota { público : void hablar () anular { std :: cout << "¡Guau! \n " ; } };             clase Gato : público Mascota { público : void hablar () anular { std :: cout << "¡Miau! \n " ; } };             // speak() podrá aceptar cualquier cosa derivada de Pet void speak ( Pet & pet ) { pet . speak (); }   int main () { Perro fido ; Gato simba ; hablar ( fido ); hablar ( simba ); devolver 0 ; }         

Véase 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, Jan (1995). "Envío de mensajes en procesadores segmentados". ECOOP'95 — Programación orientada a objetos, 9.ª conferencia europea, Åarhus, Dinamarca, 7-11 de agosto de 1995. Lecture Notes in Computer Science. Vol. 952. Springer. 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. Características de la programación orientada a objetos". El lenguaje de programación Rust (2.ª ed.). San Francisco, California, EE. UU.: No Starch Press, Inc. pp. 375–396 [379–384]. ISBN 978-1-7185-0310-6. p. 384: Los objetos de rasgo realizan un envío dinámico […] Cuando usamos objetos de rasgo, Rust debe usar un envío dinámico. El compilador no conoce todos los tipos que se pueden usar 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 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 en línea el código de un método, lo que a su vez evita algunas optimizaciones.(xxix+1+527+3 páginas)
  4. ^ "Objetos de rasgos". Referencia de Rust . Consultado el 27 de abril de 2023 .
  5. ^ Müller, Martin (1995). Envío de mensajes en lenguajes orientados a objetos con tipado dinámico (tesis de maestría). Universidad de Nuevo México. pp. 16–17. CiteSeerX 10.1.1.55.1782 . 

Lectura adicional