Paradigmas de programación |
---|
|
La programación genérica es un estilo de programación de computadoras en el que los algoritmos se escriben en términos de tipos que se especificarán más adelante y que luego se instancian cuando se necesitan para tipos específicos proporcionados como parámetros . Este enfoque, iniciado por el lenguaje de programación ML en 1973, [1] [2] permite escribir funciones o tipos comunes que difieren solo en el conjunto de tipos en los que operan cuando se usan, reduciendo así la duplicación . Estas entidades de software se conocen como genéricos en Ada , C #, Delphi , Eiffel , F # , Java , Nim , Python , Rust , Swift , TypeScript y Visual Basic .NET . Se conocen como polimorfismo paramétrico en ML , Scala , Julia y Haskell (la comunidad Haskell también usa el término "genérico" para un concepto relacionado pero algo diferente); plantillas en C ++ y D ; y tipos parametrizados en el influyente libro de 1994Patrones de diseño . [3]
El término "programación genérica" fue acuñado originalmente por David Musser y Alexander Stepanov [4] en un sentido más específico que el anterior, para describir un paradigma de programación mediante el cual los requisitos fundamentales sobre tipos se abstraen de ejemplos concretos de algoritmos y estructuras de datos y se formalizan como conceptos , con funciones genéricas implementadas en términos de estos conceptos, típicamente usando mecanismos de genéricoidad del lenguaje como se describió anteriormente.
La programación genérica se define en Musser y Stepanov (1989) de la siguiente manera,
La programación genérica se centra en la idea de abstraerse de algoritmos concretos y eficientes para obtener algoritmos genéricos que se pueden combinar con diferentes representaciones de datos para producir una amplia variedad de software útil.
- Musser, David R .; Stepanov, Alexander A., Programación genérica [5]
El paradigma de "programación genérica" es un enfoque de la descomposición de software mediante el cual los requisitos fundamentales sobre tipos se abstraen de ejemplos concretos de algoritmos y estructuras de datos y se formalizan como conceptos , de manera análoga a la abstracción de teorías algebraicas en álgebra abstracta . [6] Los primeros ejemplos de este enfoque de programación se implementaron en Scheme y Ada, [7] aunque el ejemplo más conocido es la Biblioteca de plantillas estándar (STL), [8] [9] que desarrolló una teoría de iteradores que se utiliza para desacoplar secuencias de estructuras de datos y los algoritmos que operan en ellas.
Por ejemplo, dadas N estructuras de datos de secuencia, por ejemplo, lista enlazada individualmente, vector, etc., y M algoritmos para operar sobre ellos, por ejemplo find, sortetc., un enfoque directo implementaría cada algoritmo específicamente para cada estructura de datos, dando N × M combinaciones a implementar. Sin embargo, en el enfoque de programación genérico, cada estructura de datos devuelve un modelo de un concepto de iterador (un tipo de valor simple que puede desreferenciarse para recuperar el valor actual, o cambiarse para apuntar a otro valor en la secuencia) y cada algoritmo se escribe en su lugar genéricamente con argumentos de tales iteradores, por ejemplo, un par de iteradores que apuntan al principio y al final de la subsecuencia o rangoprocesar. Por tanto, sólo es necesario implementar N + M combinaciones de estructura de datos y algoritmo. En el STL se especifican varios conceptos de iterador, cada uno de los cuales es un refinamiento de conceptos más restrictivos, por ejemplo, los iteradores de avance solo proporcionan movimiento al siguiente valor en una secuencia (por ejemplo, adecuado para una lista enlazada individualmente o un flujo de datos de entrada), mientras que un acceso aleatorio El iterador también proporciona acceso directo en tiempo constante a cualquier elemento de la secuencia (por ejemplo, adecuado para un vector). Un punto importante es que una estructura de datos devolverá un modelo del concepto más general que se puede implementar de manera eficiente: complejidad computacional.Los requisitos son explícitamente parte de la definición del concepto. Esto limita las estructuras de datos a las que se puede aplicar un algoritmo dado y estos requisitos de complejidad son un factor determinante de la elección de la estructura de datos. La programación genérica se ha aplicado de manera similar en otros dominios, por ejemplo, algoritmos de gráficos. [10]
Tenga en cuenta que aunque este enfoque a menudo utiliza características de lenguaje de genéricos / plantillas en tiempo de compilación , de hecho es independiente de los detalles técnicos del lenguaje en particular. El pionero de la programación genérica Alexander Stepanov escribió:
La programación genérica consiste en abstraer y clasificar algoritmos y estructuras de datos. Se inspira en Knuth y no en la teoría de tipos. Su objetivo es la construcción incremental de catálogos sistemáticos de estructuras de datos y algoritmos útiles, eficientes y abstractos. Tal empresa sigue siendo un sueño.
- Alexander Stepanov, Breve historia de STL [11] [12]
Creo que las teorías de los iteradores son tan fundamentales para la informática como las teorías de los anillos o los espacios de Banach son fundamentales para las matemáticas.
- Alexander Stepanov, una entrevista con A. Stepanov [13]
Bjarne Stroustrup señaló,
Siguiendo a Stepanov, podemos definir la programación genérica sin mencionar las características del lenguaje: Elevar algoritmos y estructuras de datos de ejemplos concretos a su forma más general y abstracta.
- Bjarne Stroustrup, Evolución de un lenguaje en y para el mundo real: C ++ 1991-2006 [12]
Otros paradigmas de programación que se han descrito como programación genérica incluyen la programación genérica de tipo de datos como se describe en "Programación genérica: una introducción". [14] El enfoque Scrap your boilerplate es un enfoque de programación genérico ligero para Haskell. [15]
En este artículo distinguimos los paradigmas de programación de alto nivel de la programación genérica , arriba, de los mecanismos de genéricoidad del lenguaje de programación de nivel inferior usados para implementarlos (ver Soporte de lenguaje de programación para genérico ). Para una mayor discusión y comparación de paradigmas de programación genéricos, consulte. [dieciséis]
Instalaciones Genericidad han existido en lenguajes de alto nivel por lo menos desde la década de 1970 en lenguajes como ML , CLU y Ada , y posteriormente fueron adoptados por muchos basados en objetos y orientados a objetos idiomas, incluyendo BETA , C ++ , D , Eiffel , Java , y el ahora desaparecido idioma Trellis-Owl de DEC .
Genericity se implementa y admite de manera diferente en varios lenguajes de programación; el término "genérico" también se ha utilizado de forma diferente en varios contextos de programación. Por ejemplo, en Forth el compilador puede ejecutar código mientras se compila y se pueden crear nuevas palabras clave del compilador y nuevas implementaciones para esas palabras sobre la marcha. Tiene pocas palabras que expongan el comportamiento del compilador y, por lo tanto, ofrece naturalmente capacidades de genérico que, sin embargo, no se mencionan como tales en la mayoría de los textos de Forth. De manera similar, los lenguajes tipados dinámicamente, especialmente los interpretados, generalmente ofrecen genéricode forma predeterminada, ya que tanto el paso de valores a las funciones como la asignación de valores son indiferentes al tipo y dicho comportamiento a menudo se utiliza para la abstracción o la concisión del código, sin embargo, esto no se suele etiquetar como genérico, ya que es una consecuencia directa del sistema de tipado dinámico empleado por el lenguaje. [ cita requerida ] El término se ha utilizado en programación funcional , específicamente en lenguajes similares a Haskell , que utilizan un sistema de tipos estructurales donde los tipos son siempre paramétricos y el código real de esos tipos es genérico. Estos usos todavía tienen un propósito similar de guardar código y renderizar una abstracción.
Las matrices y estructuras se pueden ver como tipos genéricos predefinidos. Cada uso de una matriz o tipo de estructura crea una instancia de un nuevo tipo concreto o reutiliza un tipo instanciado anterior. Los tipos de elementos de matriz y los tipos de elementos de estructura son tipos parametrizados, que se utilizan para instanciar el tipo genérico correspondiente. Todo esto suele estar integrado en el compilador y la sintaxis difiere de otras construcciones genéricas. Algunos lenguajes de programación extensibles intentan unificar tipos genéricos integrados y definidos por el usuario.
A continuación, se incluye un amplio estudio de los mecanismos de genéricoidad en los lenguajes de programación. Para obtener una encuesta específica que compare la idoneidad de los mecanismos para la programación genérica, consulte. [17]
Al crear clases de contenedor en lenguajes de tipo estático, es inconveniente escribir implementaciones específicas para cada tipo de datos contenido, especialmente si el código para cada tipo de datos es virtualmente idéntico. Por ejemplo, en C ++, esta duplicación de código se puede eludir definiendo una plantilla de clase:
template < typename T > class List { // Contenido de la clase. };List < Animal > list_of_animals ; List < Car > list_of_cars ;
Arriba, Thay un marcador de posición para cualquier tipo que se especifique cuando se crea la lista. Estos "contenedores de tipo T", comúnmente llamados plantillas , permiten reutilizar una clase con diferentes tipos de datos siempre que se mantengan ciertos contratos como subtipos y firma . Este mecanismo de genéricoidad no debe confundirse con el polimorfismo de inclusión , que es el uso algorítmico de subclases intercambiables: por ejemplo, una lista de objetos de tipo que Moving_Objectcontiene objetos de tipo Animaly Car. Las plantillas también se pueden utilizar para funciones independientes del tipo, como en el Swapejemplo siguiente:
// "&" denota una plantilla de referencia < typename T > void Swap ( T & a , T & b ) { // Una función similar, pero más segura y potencialmente más rápida // se define en el encabezado de la biblioteca estándar <utility> T temp = b ; b = a ; a = temp ; }std :: string world = "¡Mundo!" ; std :: string hello = "Hola" ; Swap ( mundo , hola ); std :: cout << mundo << hola << '\ n ' ; // La salida es "¡Hola, mundo!".
El templateconstructo de C ++ utilizado anteriormente se cita ampliamente [ cita requerida ] como el constructo de genéricaidad que popularizó la noción entre los programadores y diseñadores de lenguajes y admite muchos modismos de programación genéricos. El lenguaje de programación D también ofrece plantillas totalmente genéricas basadas en el precedente de C ++ pero con una sintaxis simplificada. El lenguaje de programación Java ha proporcionado recursos genéricos basados sintácticamente en C ++ desde la introducción de J2SE 5.0.
C # 2.0, Oxygene 1.5 (también conocido como Chrome) y Visual Basic .NET 2005 tienen construcciones que aprovechan el soporte para genéricos presentes en Microsoft .NET Framework desde la versión 2.0.
Ada ha tenido genéricos desde que se diseñó por primera vez en 1977-1980. La biblioteca estándar utiliza genéricos para proporcionar muchos servicios. Ada 2005 agrega una completa biblioteca de contenedores genéricos a la biblioteca estándar, que se inspiró en la biblioteca de plantillas estándar de C ++ .
Una unidad genérica es un paquete o subprograma que toma uno o más parámetros formales genéricos .
Un parámetro formal genérico es un valor, una variable, una constante, un tipo, un subprograma o incluso una instancia de otra unidad genérica designada. Para los tipos formales genéricos, la sintaxis distingue entre tipos discretos, de punto flotante, de punto fijo, de acceso (puntero), etc. Algunos parámetros formales pueden tener valores predeterminados.
Para instanciar una unidad genérica, el programador pasa parámetros reales para cada formal. La instancia genérica se comporta como cualquier otra unidad. Es posible crear instancias de unidades genéricas en tiempo de ejecución , por ejemplo, dentro de un bucle.
La especificación de un paquete genérico:
Max_Size genérico : Natural ; - un tipo de valor formal genérico Element_Type es privado ; - un tipo formal genérico; acepta cualquier paquete de tipo ilimitado. Las pilas son de tipo Size_Type tienen un rango 0 .. Max_Size ; type Stack es privado limitado ; procedimiento Crear ( S : out Stack ; Initial_Size : in Size_Type : = Max_Size ); procedimiento Empuje ( Into : in out Stack ; Element : in Element_Type ); procedimiento Pop ( From : in out Stack ; Element : out Element_Type ); Desbordamiento : excepción ; Rebase por defecto : excepción ; el subtipo privado Index_Type es Size_Type rango 1 .. Max_Size ; tipo Vector es matriz ( Rango de Index_Type <>) de Element_Type ; type Stack ( Allocated_Size : Size_Type : = 0 ) es el registro Top : Index_Type ; Almacenamiento : Vector ( 1 .. Allocated_Size ); registro final ; End Stacks ;
Creación de una instancia del paquete genérico:
type Bookmark_Type es nuevo Natural ; - registra una ubicación en el documento de texto que estamos editando el paquete Bookmark_Stacks es nuevo Stacks ( Max_Size => 20 , Element_Type => Bookmark_Type ); - Permite al usuario saltar entre ubicaciones grabadas en un documento
Usando una instancia de un paquete genérico:
type Document_Type es un registro Contenido : Ada . Cuerdas . Ilimitado . Unbounded_String ; Marcadores : Bookmark_Stacks . Pila ; registro final ; el procedimiento Editar ( Document_Name : in String ) es Document : Document_Type ; begin : inicializa la pila de marcadores: Bookmark_Stacks . Crear ( S => Documento . Marcadores , Tamaño_inicial => 10 ); - Ahora, abra el archivo Document_Name y léalo en ... end Edit ;
La sintaxis del lenguaje permite la especificación precisa de restricciones sobre parámetros formales genéricos. Por ejemplo, es posible especificar que un tipo formal genérico solo aceptará un tipo modular como real. También es posible expresar restricciones entre parámetros formales genéricos; por ejemplo:
El tipo genérico Index_Type es (<>); - debe ser un tipo discreto tipo ELEMENT_TYPE es privada ; - puede ser cualquier tipo de tipo no limitado Array_Type es una matriz ( rango de Index_Type <>) de Element_Type ;
En este ejemplo, Array_Type está restringido por Index_Type y Element_Type. Al crear una instancia de la unidad, el programador debe pasar un tipo de matriz real que satisfaga estas restricciones.
La desventaja de este control detallado es una sintaxis complicada, pero, debido a que todos los parámetros formales genéricos están completamente definidos en la especificación, el compilador puede crear instancias de genéricos sin mirar el cuerpo del genérico.
A diferencia de C ++, Ada no permite instancias genéricas especializadas y requiere que todos los genéricos sean instanciados explícitamente. Estas reglas tienen varias consecuencias:
C ++ usa plantillas para habilitar técnicas de programación genéricas. La biblioteca estándar de C ++ incluye la biblioteca de plantillas estándar o STL que proporciona un marco de plantillas para estructuras de datos y algoritmos comunes. Las plantillas en C ++ también se pueden usar para la metaprogramación de plantillas , que es una forma de evaluar previamente parte del código en tiempo de compilación en lugar de en tiempo de ejecución . Usando la especialización de plantillas, las plantillas de C ++ se consideran completas de Turing .
Hay muchos tipos de plantillas: plantillas de funciones, plantillas de clases ... Una plantilla de función es un patrón para crear funciones ordinarias basadas en los tipos de parametrización suministrados cuando se instancian. Por ejemplo, la biblioteca de plantillas estándar de C ++ contiene la plantilla de función max(x, y)que crea funciones que devuelven x o y, la que sea mayor. max()
podría definirse así:
plantilla < typename T > T max ( T x , T y ) { return x < y ? y : x ; }
Las especializaciones de esta plantilla de función, instancias con tipos específicos, se pueden llamar como una función ordinaria:
std :: cout << max ( 3 , 7 ); // Salidas 7.
El compilador examina los argumentos utilizados para llamar maxy determina que se trata de una llamada a max(int, int). A continuación, crea la instancia de una versión de la función donde el tipo de parametrización Tes int, haciendo que el equivalente de la siguiente función:
int max ( int x , int y ) { return x < y ? y : x ; }
Esto funciona si los argumentos xy yson enteros, cadenas o cualquier otro tipo para el que la expresión x < yes sensible, o más específicamente, para cualquier tipo para el que operator<se define. La herencia común no es necesaria para el conjunto de tipos que se pueden usar, por lo que es muy similar a la tipificación de pato . Un programa que define un tipo de datos personalizado puede usar la sobrecarga del operador para definir el significado de <para ese tipo, permitiendo así su uso con la max()plantilla de función. Si bien esto puede parecer un beneficio menor en este ejemplo aislado, en el contexto de una biblioteca completa como la STL, permite al programador obtener una amplia funcionalidad para un nuevo tipo de datos, simplemente definiendo algunos operadores para él. Simplemente definiendo<permite que un tipo se use con los algoritmos estándar sort(), stable_sort()y binary_search(), o que se coloque dentro de estructuras de datos como sets, montones y matrices asociativas .
Las plantillas de C ++ son completamente seguras para los tipos en tiempo de compilación. Como demostración, el tipo estándar complexno define al <operador, porque no existe un orden estricto en los números complejos . Por lo tanto, max(x, y)fallará con un error de compilación, si x y y son complexvalores. Del mismo modo, otras plantillas que se basan en <no se pueden aplicar a los complexdatos a menos que se proporcione una comparación (en forma de un funtor o función). Por ejemplo: A complexno se puede utilizar como clave para a a mapmenos que se proporcione una comparación. Desafortunadamente, los compiladores históricamente generan mensajes de error algo esotéricos, largos e inútiles para este tipo de error. Asegurarse de que un determinado objeto se adhiera a unEl protocolo del método puede aliviar este problema. Los lenguajes que usan en comparelugar de <también pueden usar complexvalores como claves.
Otro tipo de plantilla, una plantilla de clase, extiende el mismo concepto a las clases. Una especialización de plantilla de clase es una clase. Las plantillas de clase se utilizan a menudo para crear contenedores genéricos. Por ejemplo, el STL tiene un contenedor de lista enlazada . Para hacer una lista enlazada de enteros, se escribe list<int>. Se indica una lista de cadenas list<string>. A listtiene un conjunto de funciones estándar asociadas, que funcionan para cualquier tipo de parametrización compatible.
Una característica poderosa de las plantillas de C ++ es la especialización de plantillas . Esto permite que se proporcionen implementaciones alternativas basadas en ciertas características del tipo parametrizado que se está instanciando. La especialización de plantillas tiene dos propósitos: permitir ciertas formas de optimización y reducir el exceso de código.
Por ejemplo, considere un sort()función de plantilla. Una de las actividades principales que realiza dicha función es intercambiar o intercambiar los valores en dos de las posiciones del contenedor. Si los valores son grandes (en términos de la cantidad de bytes que se necesitan para almacenar cada uno de ellos), a menudo es más rápido construir primero una lista separada de punteros a los objetos, ordenar esos punteros y luego construir la secuencia ordenada final . Sin embargo, si los valores son bastante pequeños, generalmente es más rápido simplemente intercambiar los valores en el lugar según sea necesario. Además, si el tipo parametrizado ya es de algún tipo de puntero, entonces no hay necesidad de construir una matriz de punteros separada. La especialización de plantillas permite al creador de plantillas escribir diferentes implementaciones y especificar las características que deben tener los tipos parametrizados para cada implementación a utilizar.
A diferencia de las plantillas de funciones, las plantillas de clases pueden estar parcialmente especializadas . Eso significa que se puede proporcionar una versión alternativa del código de la plantilla de clase cuando se conocen algunos de los parámetros de la plantilla, dejando otros parámetros de la plantilla genéricos. Esto se puede usar, por ejemplo, para crear una implementación predeterminada (la especialización primaria ) que asume que copiar un tipo de parametrización es costoso y luego crear especializaciones parciales para tipos que son baratos de copiar, aumentando así la eficiencia general. Los clientes de dicha plantilla de clase simplemente usan especializaciones de la misma sin necesidad de saber si el compilador usó la especialización primaria o alguna especialización parcial en cada caso. Las plantillas de clase también pueden ser completamente especializadas, lo que significa que se puede proporcionar una implementación alternativa cuando se conocen todos los tipos de parametrización.
Algunos usos de las plantillas, como la max()función, se completaban previamente con macros de preprocesador similares a funciones (un legado del lenguaje de programación C ). Por ejemplo, aquí hay una posible implementación de dicha macro:
#define max (a, b) ((a) <(b)? (b): (a))
Las macros son expandidas (copia pegada) por preprocesador , antes de la compilación propiamente dicha; las plantillas son funciones reales reales. Las macros siempre se expanden en línea; Las plantillas también pueden ser funciones en línea cuando el compilador lo considere apropiado.
Sin embargo, las plantillas generalmente se consideran una mejora con respecto a las macros para estos fines. Las plantillas son de tipo seguro. Las plantillas evitan algunos de los errores comunes que se encuentran en el código que hace un uso intensivo de macros similares a funciones, como evaluar parámetros con efectos secundarios dos veces. Quizás lo más importante es que las plantillas se diseñaron para ser aplicables a problemas mucho mayores que las macros.
Hay cuatro inconvenientes principales en el uso de plantillas: características compatibles, compatibilidad con el compilador, mensajes de error deficientes (generalmente con SFINAE anterior a C ++ 20) y exceso de código :
Entonces, ¿se puede usar la derivación para reducir el problema del código replicado porque se usan plantillas? Esto implicaría derivar una plantilla de una clase ordinaria. Esta técnica demostró ser exitosa para frenar la hinchazón del código en el uso real. Las personas que no utilizan una técnica como esta han descubierto que el código replicado puede costar megabytes de espacio de código incluso en programas de tamaño moderado.
- Bjarne Stroustrup , El diseño y evolución de C ++, 1994 [19]
Las instancias adicionales generadas por las plantillas también pueden hacer que algunos depuradores tengan dificultades para trabajar correctamente con las plantillas. Por ejemplo, establecer un punto de interrupción de depuración dentro de una plantilla desde un archivo de origen puede perder la configuración del punto de interrupción en la instanciación real deseada o puede establecer un punto de interrupción en cada lugar donde se instancia la plantilla.
Además, el código fuente de implementación para la plantilla debe estar completamente disponible (por ejemplo, incluido en un encabezado) para la unidad de traducción (archivo fuente) que lo utiliza. Las plantillas, incluida gran parte de la biblioteca estándar, si no se incluyen en los archivos de encabezado, no se pueden compilar. (Esto contrasta con el código sin plantilla, que puede compilarse en binario, proporcionando solo un archivo de encabezado de declaraciones para el código que lo usa). Esto puede ser una desventaja al exponer el código de implementación, que elimina algunas abstracciones y podría restringir su uso en proyectos de código cerrado. [ cita requerida ]
El lenguaje de programación D admite plantillas basadas en diseño en C ++. La mayoría de los modismos de las plantillas de C ++ se transferirán a D sin alteraciones, pero D agrega algunas funciones adicionales:
if constexpr
.Las plantillas de D utilizan una sintaxis diferente que en C ++: mientras que en los parámetros de plantilla C ++ son envueltos entre paréntesis angulares ( Template<param1, param2>), D utiliza un signo de exclamación y paréntesis: Template!(param1, param2). Esto evita las dificultades de análisis de C ++ debido a la ambigüedad con los operadores de comparación. Si solo hay un parámetro, se pueden omitir los paréntesis.
Convencionalmente, D combina las características anteriores para proporcionar polimorfismo en tiempo de compilación utilizando programación genérica basada en rasgos. Por ejemplo, un rango de entrada se define como cualquier tipo que satisfaga las comprobaciones realizadas por isInputRange, que se define de la siguiente manera:
plantilla isInputRange ( R ) { enum bool isInputRange = es ( typeof ( ( inout int = 0 ) { R r = R . init ; // puede definir un objeto de rango si ( r . vaciar ) {} // puede probar para vacío r . popFront (); // puede invocar popFront () auto h = r . front ; // puede obtener el frente del rango })); }
Una función que acepta solo rangos de entrada puede usar la plantilla anterior en una restricción de plantilla:
auto fun ( Rango ) ( Rango de rango ) if ( isInputRange ! Range ) { // ... }
Además de la metaprogramación de plantillas, D también proporciona varias características para permitir la generación de código en tiempo de compilación:
La combinación de lo anterior permite generar código basado en declaraciones existentes. Por ejemplo, los marcos de serialización de D pueden enumerar los miembros de un tipo y generar funciones especializadas para cada tipo serializado para realizar la serialización y deserialización. Los atributos definidos por el usuario podrían indicar además las reglas de serialización.
La importejecución de la función de expresión y tiempo de compilación también permite implementar de manera eficiente lenguajes específicos del dominio . Por ejemplo, dada una función que toma una cadena que contiene una plantilla HTML y devuelve un código fuente D equivalente, es posible usarla de la siguiente manera:
// Importa el contenido de example.htt como una constante de manifiesto de cadena. enum htmlTemplate = import ( "ejemplo.htt" );// Transpile la plantilla HTML a código D. enumeración htmlDCode = htmlTemplateToD ( htmlTemplate );// Pegue el contenido de htmlDCode como código D. mixin ( htmlDCode );
Las clases genéricas han sido parte de Eiffel desde el método original y el diseño del lenguaje. Las publicaciones de la Fundación de Eiffel, [21] [22] utilizan el término genericity para describir la creación y uso de clases genéricas.
Las clases genéricas se declaran con su nombre de clase y una lista de uno o más parámetros genéricos formales . En el siguiente código, la clase LIST
tiene un parámetro genérico formalG
clase LISTA [ G ] ... característica - Acceso artículo : G - El elemento actualmente apuntado por el cursor ... característica - elemento de cambio de puesto ( NEW_ITEM : G ) - Añadir `NEW_ITEM' al final de la lista ...
Los parámetros genéricos formales son marcadores de posición para nombres de clases arbitrarios que se proporcionarán cuando se haga una declaración de la clase genérica, como se muestra en las dos derivaciones genéricas a continuación, donde ACCOUNT
y DEPOSIT
son otros nombres de clase. ACCOUNT
y DEPOSIT
se consideran parámetros genéricos reales, ya que proporcionan nombres de clases reales para sustituir G
en el uso real.
list_of_accounts : LIST [ ACCOUNT ] - Lista de cuentas list_of_deposits : LISTA [ DEPÓSITO ] - Lista de Depósito
Dentro del sistema de tipos de Eiffel, aunque la clase LIST [G]
se considera una clase, no se considera un tipo. Sin embargo, una derivación genérica de LIST [G]
tal como LIST [ACCOUNT]
se considera un tipo.
Para la clase de lista que se muestra arriba, un parámetro genérico real que sustituye G
puede ser cualquier otra clase disponible. Para restringir el conjunto de clases de las que se pueden elegir parámetros genéricos reales válidos, se puede especificar una restricción genérica . En la declaración de clase a SORTED_LIST
continuación, la restricción genérica dicta que cualquier parámetro genérico real válido será una clase que hereda de la clase COMPARABLE
. La restricción genérica asegura que los elementos de a SORTED_LIST
puedan, de hecho, ser ordenados.
clase SORTED_LIST [ G -> COMPARABLE ]
El soporte para los genéricos , o "contenedores de tipo-T" se agregó al lenguaje de programación Java en 2004 como parte de J2SE 5.0. En Java, los genéricos solo se verifican en el momento de la compilación para verificar la corrección de tipos. La información de tipo genérico se elimina a través de un proceso llamado borrado de tipo , para mantener la compatibilidad con implementaciones de JVM antiguas, lo que la hace no disponible en tiempo de ejecución. Por ejemplo, a List<String>se convierte al tipo sin formato List. El compilador inserta conversiones de tipos para convertir los elementos al Stringtipo cuando se recuperan de la lista, lo que reduce el rendimiento en comparación con otras implementaciones, como las plantillas de C ++.
Los genéricos se agregaron como parte de .NET Framework 2.0 en noviembre de 2005, sobre la base de un prototipo de investigación de Microsoft Research iniciado en 1999. [23] Aunque son similares a los genéricos en Java, los genéricos .NET no aplican borrado de tipos , pero implementan genéricos como un Mecanismo de primera clase en tiempo de ejecución mediante reificación . Esta opción de diseño proporciona una funcionalidad adicional, como permitir la reflexión con preservación de tipos genéricos, así como aliviar algunas de las limitaciones del borrado (como no poder crear matrices genéricas). [24] [25] Esto también significa que no hay impacto en el rendimiento de los lanzamientos en tiempo de ejecución y normalmente es costosoconversiones de boxeo . Cuando los tipos primitivos y de valor se utilizan como argumentos genéricos, obtienen implementaciones especializadas, lo que permite colecciones y métodos genéricos eficientes . Como en C ++ y Java, los tipos genéricos anidados como Dictionary <string, List <int>> son tipos válidos, sin embargo, se desaconsejan para las firmas de miembros en las reglas de diseño de análisis de código. [26]
.NET permite seis variedades de restricciones de tipo genérico utilizando la wherepalabra clave, incluida la restricción de tipos genéricos para que sean tipos de valor, clases, constructores e implementación de interfaces. [27] A continuación se muestra un ejemplo con una restricción de interfaz:
usando el sistema ; muestra de clase{ vacío estático Main () { int [] matriz = { 0 , 1 , 2 , 3 }; MakeAtLeast < int > ( matriz , 2 ); // Cambia la matriz a {2, 2, 2, 3} foreach ( int i en matriz ) Consola . WriteLine ( i ); // Imprimir resultados. Consola . ReadKey ( verdadero ); } vacío estático MakeAtLeast < T > ( lista T [] , T más bajo ) donde T : IComparable < T > { para ( int i = 0 ; i < lista . Longitud ; i ++) si ( lista [ i ]. CompareTo ( más bajo ) < 0 ) lista [ i ] = más bajo ; }}
El MakeAtLeast()método permite operar sobre matrices, con elementos de tipo genérico T. La restricción de tipo del método indica que el método es aplicable a cualquier tipo Tque implemente la IComparable<T>interfaz genérica . Esto asegura un error de tiempo de compilación , si se llama al método si el tipo no admite la comparación. La interfaz proporciona el método genérico CompareTo(T).
El método anterior también podría escribirse sin tipos genéricos, simplemente usando el tipo no genérico Array. Sin embargo, dado que las matrices son contravariantes , la conversión no sería segura para los tipos y el compilador no podría encontrar ciertos errores posibles que de otro modo se detectarían al usar tipos genéricos. Además, el método necesitaría acceder a los elementos de la matriz como objects en su lugar, y requeriría la conversión para comparar dos elementos. (Para tipos de valor como tipos como inteste, se requiere una conversión de caja , aunque esto se puede solucionar usando la Comparer<T>clase, como se hace en las clases de colección estándar).
Un comportamiento notable de los miembros estáticos en una clase .NET genérica es la instanciación de miembros estáticos por tipo de tiempo de ejecución (consulte el ejemplo a continuación).
// Una clase genérica public class GenTest < T > { // Se creará una variable estática - para cada tipo en la reflexión static CountedInstances OnePerType = new CountedInstances (); // un miembro de datos privado T mT ; // constructor simple public GenTest ( T pT ) { mT = pT ; } } // una clase public class CountedInstances { // Variable estática - esto se incrementará una vez por instancia public static int Counter ; // constructor simple public CountedInstances () { // aumenta el contador en uno durante la instanciación del objeto CountedInstances . Counter ++; } } // punto de entrada del código principal // al final de la ejecución, CountedInstances.Counter = 2 GenTest < int > g1 = new GenTest < int > ( 1 ); GenTest < int > g11 = new GenTest < int > ( 11 ); GenTest < int > g111 = new GenTest < int > ( 111 ); GenTest < doble > g2 = nuevo GenTest< doble > ( 1,0 );
El dialecto Object Pascal de Delphi adquirió genéricos en la versión de Delphi 2007, inicialmente solo con el compilador .NET (ahora descontinuado) antes de agregarse al código nativo en la versión de Delphi 2009. La semántica y las capacidades de los genéricos de Delphi se basan en gran medida en las que tenían los genéricos en .NET 2.0, aunque la implementación es por necesidad bastante diferente. Aquí hay una traducción más o menos directa del primer ejemplo de C # que se muestra arriba:
Muestra de programa ;{$ APPTYPE CONSOLE}usa genéricos . Incumplimientos ; // para IComparer <>tipo TUtils = clase clase procedimiento MakeAtLeast < T > ( Arr : TArray < T >; const Lowest : T ; Comparer : IComparer < T > ) ; sobrecarga ; procedimiento de clase MakeAtLeast < T > ( Arr : TArray < T >; const Lowest : T ) ; sobrecarga ; terminar ; procedimiento de clase TUtils . MakeAtLeast < T > ( Arr : TArray < T >; const Lowest : T ; Comparer : IComparer < T > ) ; var I : Entero ; comience si Comparer = nil entonces Comparer : = TComparer < T >. Predeterminado ; para I : = Bajo ( Arr) a Alto ( Arr ) hacer si Comparer . Compare ( Arr [ I ] , Lowest ) < 0 luego Arr [ I ] : = Lowest ; terminar ; procedimiento de clase TUtils . MakeAtLeast < T > ( Arr : TArray < T >; const Lowest : T ) ; comenzar MakeAtLeast < T > ( Arr , Lowest , nil ) ; terminar ;var Ints : TArray < Entero >; Valor : entero ; begin Ints : = TArray < Integer >. Crear ( 0 , 1 , 2 , 3 ) ; TUtils . MakeAtLeast < Entero > ( Ints , 2 ) ; de Valor en Entrs hacer WriteLn ( Valor ) ; ReadLn ; fin.
Al igual que con C #, tanto los métodos como los tipos completos pueden tener uno o más parámetros de tipo. En el ejemplo, TArray es un tipo genérico (definido por el lenguaje) y MakeAtLeast un método genérico. Las restricciones disponibles son muy similares a las restricciones disponibles en C #: cualquier tipo de valor, cualquier clase, una clase o interfaz específica y una clase con un constructor sin parámetros. Varias restricciones actúan como una unión aditiva.
Free Pascal implementó genéricos antes que Delphi, y con diferente sintaxis y semántica. Sin embargo, desde la versión 2.6.0 de FPC, la sintaxis estilo Delphi está disponible cuando se usa el modo de idioma {$ mode Delphi}. Por lo tanto, los programadores de Free Pascal pueden usar genéricos en el estilo que prefieran.
Ejemplo de Delphi y Free Pascal:
// Unidad de estilo Delphi A ;{$ ifdef fpc} {$ mode delphi} {$ endif}interfaztipo TGenericClass < T > = función de clase Foo ( const AValue : T ) : T ; terminar ; implementaciónfunción TGenericClass < T >. Foo ( const AValue : T ) : T ; comenzar Resultado : = AValue + AValue ; terminar ;final .// Unidad B de estilo ObjFPC de Free Pascal ;{$ ifdef fpc} {$ mode objfpc} {$ endif}interfaztipo genérico TGenericClass < T > = función de clase Foo ( const AValue : T ) : T ; terminar ; implementaciónfunción TGenericClass . Foo ( const AValue : T ) : T ; comenzar Resultado : = AValue + AValue ; terminar ;final .// ejemplo de uso, programa estilo Delphi TestGenDelphi ;{$ ifdef fpc} {$ mode delphi} {$ endif}usa A , B ;var GC1 : Una . TGenericClass < Entero >; GC2 : B . TGenericClass < String >; comenzará GC1 : = A . TGenericClass < Entero >. Crear ; GC2 : = B . TGenericClass < Cadena >. Crear ; WriteLn ( GC1 . Foo ( 100 )) ; // 200 WriteLn ( GC2 .Foo ( 'hola' )) ; // hola hola GC1 . Libre ; GC2 . Libre ; final .// ejemplo de uso, programa de estilo ObjFPC TestGenDelphi ;{$ ifdef fpc} {$ mode objfpc} {$ endif}usa A , B ;// requerida en ObjFPC tipo TAGenericClassInt = especializarse A . TGenericClass < Entero >; TBGenericClassString = especializarse B . TGenericClass < String >; var GC1 : TAGenericClassInt ; GC2 : TBGenericClassString ; comience GC1 : = TAGenericClassInt . Crear ; GC2 : = TBGenericClassString . Crear ; WriteLn ( GC1. Foo ( 100 )) ; // 200 WriteLn ( GC2 . Foo ( 'hola' )) ; // hola hola GC1 . Libre ; GC2 . Libre ; final .
El mecanismo de clases de tipos de Haskell admite programación genérica. Seis de las clases de tipos predefinidas en Haskell (incluidos Eqlos tipos que se pueden comparar por igualdad y Showlos tipos cuyos valores se pueden representar como cadenas) tienen la propiedad especial de admitir instancias derivadas. Esto significa que un programador que defina un nuevo tipo puede afirmar que este tipo será una instancia de una de estas clases de tipos especiales, sin proporcionar implementaciones de los métodos de clase, como suele ser necesario al declarar instancias de clase. Todos los métodos necesarios se "derivarán", es decir, se construirán automáticamente, basándose en la estructura del tipo. Por ejemplo, la siguiente declaración de un tipo de árboles binariosestablece que debe ser una instancia de las clases Eqy Show:
data BinTree a = Leaf a | Nodo ( BinTree a ) a ( BinTree a ) derivando ( Eq , Show )
Esto da como resultado una función de igualdad ( ==) y una función de representación de cadena ( show) que se definen automáticamente para cualquier tipo de formulario BinTree Tsiempre que Tadmita esas operaciones.
El apoyo para los casos derivados de Eqy Showhace que sus métodos ==y showgenérica de una manera cualitativamente diferente de funciones para-métricamente polimórfica: estas "funciones" (más exactamente, las familias de tipo indexado de funciones) se pueden aplicar a los valores de varios tipos, y aunque se comportan de manera diferente para cada tipo de argumento, se necesita poco trabajo para agregar soporte para un nuevo tipo. Ralf Hinze (2004) ha demostrado que se puede lograr un efecto similar para las clases de tipos definidas por el usuario mediante ciertas técnicas de programación. Otros investigadores han propuesto enfoques para este y otros tipos de genérico en el contexto de Haskell y extensiones de Haskell (discutido a continuación).
PolyP fue la primera extensión de lenguaje de programación genérico de Haskell . En PolyP, las funciones genéricas se denominan politípicas . El lenguaje introduce una construcción especial en la que tales funciones politípicas se pueden definir mediante inducción estructural sobre la estructura del functor de patrón de un tipo de datos regular. Los tipos de datos regulares en PolyP son un subconjunto de los tipos de datos de Haskell. Un tipo de datos regular t debe ser del tipo * → * , y si a es el argumento de tipo formal en la definición, entonces todas las llamadas recursivas a t deben tener la forma ta. Estas restricciones descartan los tipos de datos de tipo superior, así como los tipos de datos anidados, donde las llamadas recursivas son de una forma diferente. La función aplanar en PolyP se proporciona aquí como ejemplo:
aplanar :: Regular d => d a -> [ a ] aplanar = cata fl politípico fl :: f a [ a ] -> [ a ] caso f de g + h -> ya sea fl fl g * h -> \ ( x , y ) -> fl x ++ fl y () -> \ x -> [] Par -> \ x -> [ x ] Rec -> \ x -> x d @ g -> concat . aplanar . pmap fl Con t -> \ x -> [] cata :: Regular d => ( FunctorOf d a b -> b ) -> d a -> b
Haskell genérico es otra extensión de Haskell , desarrollado en la Universidad de Utrecht en los Países Bajos . Las extensiones que proporciona son:
El valor indexado por tipo resultante se puede especializar en cualquier tipo.
Como ejemplo, la función de igualdad en Generic Haskell: [28]
escriba la ecuación {[ * ]} t1 t2 = t1 -> t2 -> Bool tipo Eq {[ k -> l ]} t1 t2 = forall u1 u2 . Ecuación {[ k ]} u1 u2 -> Ecuación {[ l ]} ( t1 u1 ) ( t2 u2 ) eq { | t :: k | } :: Eq {[ k ]} t t eq { | Unidad | } _ _ = True eq { | : +: | } eqA eqB ( Inl a1 ) ( Inl a2 ) = eqA a1 a2 eq { | : +: | } eqA eqB ( Inr b1 ) (Inr b2 ) = eqB b1 b2 eq { | : +: | } eqA eqB _ _ = False eq { | : *: | } eqA eqB ( a1 : *: b1 ) ( a2 : *: b2 ) = eqA a1 a2 && eqB b1 b2 eq { | Int | } = ( == ) eq { | Char | } = ( == ) eq { | Bool | } = ( == )
Clean ofrece PolyP genérico basado en programación y Haskell genérico compatible con GHC> = 6.0. Parametriza por tipo como esos pero ofrece sobrecarga.
Los lenguajes de la familia ML admiten la programación genérica mediante polimorfismo paramétrico y módulos genéricos denominados functores. Tanto Standard ML como OCaml proporcionan functors, que son similares a las plantillas de clase y a los paquetes genéricos de Ada. Las abstracciones sintácticas de esquemas también tienen una conexión con la genéricaidad; de hecho, son un superconjunto de plantillas de C ++.
Un módulo Verilog puede tomar uno o más parámetros, a los que se asignan sus valores reales tras la instanciación del módulo. Un ejemplo es una matriz de registro genérica donde el ancho de la matriz se da a través de un parámetro. Una matriz de este tipo, combinada con un vector de cable genérico, puede crear un búfer genérico o un módulo de memoria con un ancho de bits arbitrario a partir de una implementación de módulo único. [29]
VHDL , derivado de Ada, también tiene capacidades genéricas. [ cita requerida ]
C admite "expresiones genéricas de tipo" con la _Generic
palabra clave: [30]
#define cbrt (x) _Generic ((x), long double: cbrtl, \ default: cbrt, \ float: cbrtf) (x)
|journal=
( ayuda )