multiple - ¿Cuáles son las diferencias entre los genéricos en C#y Java... y las plantillas en C++?
generics restrictions c# (13)
Yo uso principalmente Java y los genéricos son relativamente nuevos. Sigo leyendo que Java tomó la decisión equivocada o que .NET tiene mejores implementaciones, etc., etc.
Entonces, ¿cuáles son las principales diferencias entre C ++, C # y Java en los genéricos? ¿Pros / contras de cada uno?
11 meses tarde, pero creo que esta pregunta está lista para algunas cosas de Java Wildcard.
Esta es una característica sintáctica de Java. Supongamos que tienes un método:
public <T> void Foo(Collection<T> thing)
Y suponga que no necesita referirse al tipo T en el cuerpo del método. Está declarando un nombre T y luego solo lo usa una vez, así que ¿por qué debería pensar en un nombre para él? En su lugar, puede escribir:
public void Foo(Collection<?> thing)
El signo de interrogación le pide al compilador que simule que usted declaró un parámetro de tipo con nombre normal que solo necesita aparecer una vez en ese lugar.
No hay nada que pueda hacer con los comodines que no pueda hacer con un parámetro de tipo nombrado (que es como estas cosas se hacen siempre en C ++ y C #).
C ++ rara vez utiliza la terminología "genéricos". En cambio, la palabra "plantillas" se usa y es más precisa. Las plantillas describen una técnica para lograr un diseño genérico.
Las plantillas de C ++ son muy diferentes de lo que implementan C # y Java por dos razones principales. La primera razón es que las plantillas de C ++ no solo permiten argumentos de tipo de tiempo de compilación sino también argumentos de valor constante de tiempo de compilación: las plantillas se pueden dar como enteros o incluso como firmas de función. Esto significa que puede hacer algunas cosas bastante extrañas en tiempo de compilación, por ejemplo, cálculos:
template <unsigned int N>
struct product {
static unsigned int const VALUE = N * product<N - 1>::VALUE;
};
template <>
struct product<1> {
static unsigned int const VALUE = 1;
};
// Usage:
unsigned int const p5 = product<5>::VALUE;
Este código también utiliza la otra característica distinguida de las plantillas de C ++, a saber, la especialización de plantillas. El código define una plantilla de clase, product
que tiene un argumento de valor. También define una especialización para esa plantilla que se usa cuando el argumento se evalúa como 1. Esto me permite definir una recursión sobre las definiciones de plantillas. Creo que esto fue descubierto por primera vez por Andrei Alexandrescu .
La especialización de plantillas es importante para C ++ porque permite diferencias estructurales en las estructuras de datos. Las plantillas en su conjunto son un medio para unificar una interfaz entre tipos. Sin embargo, aunque esto es deseable, todos los tipos no pueden tratarse por igual dentro de la implementación. Las plantillas de C ++ tienen esto en cuenta. Esta es la misma diferencia que OOP hace entre la interfaz y la implementación con el reemplazo de los métodos virtuales.
Las plantillas de C ++ son esenciales para su paradigma de programación algorítmica. Por ejemplo, casi todos los algoritmos para contenedores se definen como funciones que aceptan el tipo de contenedor como un tipo de plantilla y los tratan de manera uniforme. En realidad, eso no es del todo correcto: C ++ no funciona en contenedores, sino en rangos definidos por dos iteradores, que apuntan al principio y detrás del final del contenedor. Por lo tanto, todo el contenido está circunscrito por los iteradores: begin <= elements <end.
Usar iteradores en lugar de contenedores es útil porque permite operar en partes de un contenedor en lugar de en general.
Otra característica distintiva de C ++ es la posibilidad de una especialización parcial para las plantillas de clase. Esto está algo relacionado con la coincidencia de patrones en los argumentos en Haskell y otros lenguajes funcionales. Por ejemplo, consideremos una clase que almacena elementos:
template <typename T>
class Store { … }; // (1)
Esto funciona para cualquier tipo de elemento. Pero digamos que podemos almacenar punteros más eficientemente que otros tipos aplicando algún truco especial. Podemos hacerlo parcialmente especializando para todos los tipos de punteros:
template <typename T>
class Store<T*> { … }; // (2)
Ahora, siempre que instalemos una plantilla de contenedor para un tipo, se usa la definición apropiada:
Store<int> x; // Uses (1)
Store<int*> y; // Uses (2)
Store<string**> z; // Uses (2), with T = string*.
El mismo Anders Hejlsberg describió las diferencias aquí " Genéricos en C #, Java y C ++ ".
En Java, los genéricos son solo a nivel de compilador, así que obtienes:
a = new ArrayList<String>()
a.getClass() => ArrayList
Tenga en cuenta que el tipo de ''a'' es una lista de matriz, no una lista de cadenas. Entonces, el tipo de una lista de bananas sería igual a () una lista de monos.
Por así decirlo.
La queja más grande es el borrado de tipo. En eso, los genéricos no se aplican en tiempo de ejecución. Aquí hay un enlace a algunos documentos de Sun sobre el tema .
Los genéricos se implementan mediante el borrado de tipo: la información de tipo genérico está presente solo en el momento de la compilación, después de lo cual el compilador la borra.
Las plantillas de C ++ son en realidad mucho más poderosas que sus contrapartes de C # y Java, ya que se evalúan en el momento de la compilación y la especialización de soporte. Esto permite la Meta-Programación de plantillas y hace que el compilador de C ++ sea equivalente a una máquina de Turing (es decir, durante el proceso de compilación puede calcular todo lo que sea computable con una máquina de Turing).
NB: No tengo suficiente punto para comentar, así que siéntase libre de mover esto como un comentario a la respuesta apropiada.
Contrariamente a la creencia popular, que nunca entendí de dónde vino, .net implementó verdaderos genéricos sin romper la compatibilidad con versiones anteriores, y dedicaron un esfuerzo explícito para eso. No tiene que cambiar su código .net 1.0 no genérico a genéricos solo para ser usado en .net 2.0. Tanto las listas genéricas como las no genéricas aún están disponibles en .Net framework 2.0 hasta 4.0, exactamente por nada más que por razones de compatibilidad con versiones anteriores. Por lo tanto, los códigos antiguos que todavía usaban ArrayList no genérico seguirán funcionando y usarán la misma clase de ArrayList que antes. La compatibilidad con el código anterior siempre se mantiene desde la versión 1.0 hasta ahora ... Por lo tanto, incluso en .net 4.0, aún tiene la opción de usar cualquier clase no genérica de la versión 1.0 BCL si decide hacerlo.
Así que no creo que Java tenga que romper la compatibilidad con versiones anteriores para admitir los genéricos verdaderos.
Parece que, entre otras propuestas muy interesantes, hay una sobre el refinamiento de los genéricos y la ruptura de la compatibilidad hacia atrás:
Actualmente, los genéricos se implementan utilizando el borrado, lo que significa que la información de tipo genérico no está disponible en tiempo de ejecución, lo que dificulta la escritura de algún tipo de código. Los genéricos se implementaron de esta manera para admitir la compatibilidad con versiones anteriores con código no genérico anterior. Los genéricos de Reified harían que la información de tipo genérico estuviera disponible en tiempo de ejecución, lo que rompería el código no genérico heredado. Sin embargo, Neal Gafter ha propuesto que los tipos sean confiables solo si se especifican, para no interrumpir la compatibilidad hacia atrás.
en el artículo de Alex Miller sobre las propuestas de Java 7
Seguimiento a mi publicación anterior.
Las plantillas son una de las razones principales por las que C ++ falla tan abismalmente en intellisense, independientemente del IDE utilizado. Debido a la especialización de la plantilla, el IDE nunca puede estar realmente seguro de si un miembro dado existe o no. Considerar:
template <typename T>
struct X {
void foo() { }
};
template <>
struct X<int> { };
typedef int my_int_type;
X<my_int_type> a;
a.|
Ahora, el cursor está en la posición indicada y es muy difícil que el IDE diga en ese punto si, y qué, tiene a
miembro. Para otros idiomas, el análisis sería sencillo, pero para C ++, se necesita un poco de evaluación de antemano.
Se pone peor. ¿Qué my_int_type
si my_int_type
se definió dentro de una plantilla de clase? Ahora su tipo dependería de otro tipo de argumento. Y aquí, incluso los compiladores fallan.
template <typename T>
struct Y {
typedef T my_type;
};
X<Y<int>::my_type> b;
Después de pensar un poco, un programador podría concluir que este código es el mismo que el anterior: Y<int>::my_type
resuelve en int
, por lo tanto, b
debería ser del mismo tipo que a
, ¿verdad?
Incorrecto. En el punto en que el compilador intenta resolver esta afirmación, ¡en realidad aún no conoce Y<int>::my_type
! Por lo tanto, no sabe que este es un tipo. Podría ser otra cosa, por ejemplo, una función miembro o un campo. Esto podría dar lugar a ambigüedades (aunque no en el presente caso), por lo tanto, el compilador falla. Tenemos que decirle explícitamente que nos referimos a un nombre de tipo:
X<typename Y<int>::my_type> b;
Ahora, el código compila. Para ver cómo surgen las ambigüedades de esta situación, considere el siguiente código:
Y<int>::my_type(123);
Esta declaración de código es perfectamente válida y le dice a C ++ que ejecute la llamada a la función a Y<int>::my_type
. Sin embargo, si my_type
no es una función sino un tipo, esta declaración aún sería válida y realizaría una my_type
especial (la conversión de estilo de función) que a menudo es una invocación del constructor. El compilador no puede decir a qué nos referimos, por lo que debemos desambiguar aquí.
Tanto Java como C # introdujeron los genéricos después de su primer lanzamiento en el idioma. Sin embargo, hay diferencias en cómo las bibliotecas centrales cambiaron cuando se introdujeron los genéricos. Los genéricos de C # no son solo la magia del compilador, por lo que no fue posible generar las clases de bibliotecas existentes sin romper la compatibilidad hacia atrás.
Por ejemplo, en Java, el marco de colecciones existente fue completamente genérico . Java no tiene una versión genérica y no genérica de las clases de colecciones. En cierto modo, esto es mucho más limpio: si necesita usar una colección en C #, en realidad hay muy pocas razones para usar la versión no genérica, pero esas clases heredadas permanecen en su lugar, saturando el paisaje.
Otra diferencia notable es la clase Enum en Java y C #. Enum de Java tiene esta definición de aspecto algo tortuoso:
// java.lang.Enum Definition in Java
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
(vea la explicación muy clara de Angelika Langer de por qué exactamente esto es así. Básicamente, esto significa que Java puede dar acceso de tipo seguro desde una cadena a su valor Enum:
// Parsing String to Enum in Java
Colour colour = Colour.valueOf("RED");
Compare esto con la versión de C #:
// Parsing String to Enum in C#
Colour colour = (Colour)Enum.Parse(typeof(Colour), "RED");
Como Enum ya existía en C # antes de que se introdujeran los genéricos en el lenguaje, la definición no podía cambiar sin romper el código existente. Entonces, al igual que las colecciones, permanece en las bibliotecas centrales en este estado heredado.
Wikipedia tiene excelentes escritos que comparan tanto los genéricos de Java / C # como los genéricos de Java / plantillas de C ++ . El artículo principal sobre Genéricos parece un poco abarrotado, pero tiene buena información.
Ya hay muchas respuestas buenas sobre cuáles son las diferencias, así que permítanme dar una perspectiva ligeramente diferente y agregar el por qué .
Como ya se explicó, la principal diferencia es el borrado de tipo , es decir, el hecho de que el compilador de Java borra los tipos genéricos y no terminan en el bytecode generado. Sin embargo, la pregunta es: ¿por qué alguien haría eso? ¡No tiene sentido! O lo hace?
Bueno, ¿cuál es la alternativa? Si no implementas genéricos en el lenguaje, ¿ dónde los implementas? Y la respuesta es: en la Máquina Virtual. Lo que rompe la compatibilidad hacia atrás.
El borrado de tipos, por otro lado, le permite mezclar clientes genéricos con bibliotecas no genéricas. En otras palabras: el código compilado en Java 5 todavía puede implementarse en Java 1.4.
Microsoft, sin embargo, decidió romper la compatibilidad hacia atrás para los genéricos. Es por eso que .NET Generics es "mejor" que Java Generics.
Por supuesto, el sol no son idiotas o cobardes. La razón por la que se "acobardaron" fue que Java era significativamente más antiguo y más extendido que .NET cuando introdujeron los genéricos. (Fueron introducidos aproximadamente al mismo tiempo en ambos mundos). Romper la compatibilidad hacia atrás hubiera sido un gran dolor.
Dicho de otra manera: en Java, los genéricos son parte del lenguaje (lo que significa que se aplican solo a Java, no a otros idiomas), en .NET son parte de la máquina virtual (lo que significa que se aplican a todos los idiomas, no solo C # y Visual Basic.NET).
Compare esto con características de .NET como LINQ, expresiones lambda, inferencia de tipo de variable local, tipos anónimos y árboles de expresión: estas son todas las características del idioma . Es por eso que hay diferencias sutiles entre VB.NET y C #: si esas características fueran parte de la VM, serían las mismas en todos los idiomas. Pero el CLR no ha cambiado: sigue siendo el mismo en .NET 3.5 SP1 que en .NET 2.0. Puede compilar un programa de C # que use LINQ con el compilador .NET 3.5 y seguir ejecutándolo en .NET 2.0, siempre que no use ninguna biblioteca .NET 3.5. Eso no funcionaría con los genéricos y .NET 1.1, pero funcionaría con Java y Java 1.4.
Agregaré mi voz al ruido y intentaré aclarar las cosas:
C # Generics te permite declarar algo como esto.
List<Person> foo = new List<Person>();
y luego el compilador evitará que pongas cosas que no son Person
en la lista.
Detrás de escena, el compilador de C # simplemente coloca la List<Person>
en el archivo .NET dll, pero en el tiempo de ejecución, el compilador JIT va y crea un nuevo conjunto de código, como si hubiera escrito una lista especial para contener personas, algo como ListOfPerson
.
El beneficio de esto es que lo hace realmente rápido. No hay casting ni ninguna otra cosa, y debido a que la dll contiene la información de que se trata de una Lista de Person
, otro código que la examina más adelante utilizando la reflexión puede indicar que contiene objetos Person
(para que tenga inteligencia y así sucesivamente).
La desventaja de esto es que el antiguo código C # 1.0 y 1.1 (antes de que agregaran los genéricos) no comprende esta nueva List<something>
, por lo que tiene que convertir manualmente las cosas de nuevo en la List
antigua para interoperar con ellas. Esto no es un problema tan grande, porque el código binario C # 2.0 no es compatible con versiones anteriores. La única vez que esto sucederá es si está actualizando algún código antiguo de C # 1.0 / 1.1 a C # 2.0
Java Generics te permite declarar algo como esto.
ArrayList<Person> foo = new ArrayList<Person>();
En la superficie se ve igual, y de alguna manera es. El compilador también evitará que pongas cosas que no son Person
en la lista.
La diferencia es lo que sucede detrás de escena. A diferencia de C #, Java no va a construir un ListOfPerson
especial, solo usa el ArrayList
antiguo que siempre ha estado en Java. Cuando saca cosas de la matriz, la Person p = (Person)foo.get(1);
Todavía hay que hacer casting-dance. El compilador le está guardando las pulsaciones de teclas, pero la velocidad de golpe / lanzamiento se incurre como siempre lo fue.
Cuando la gente menciona "Tipo de Borrado", esto es de lo que están hablando. El compilador inserta los lanzamientos por usted, y luego ''borra'' el hecho de que está destinado a ser una lista de Person
no solo de Object
El beneficio de este enfoque es que el código antiguo que no comprende los genéricos no tiene que preocuparse. Todavía se trata de la misma vieja ArrayList
como siempre lo ha hecho. Esto es más importante en el mundo de Java porque querían admitir compilar código usando Java 5 con genéricos y ejecutarlo en versiones anteriores o anteriores de JVM, con lo que Microsoft decidió deliberadamente no molestarse.
El inconveniente es el golpe de velocidad que mencioné anteriormente, y también porque no hay ListOfPerson
pseudo-clase ListOfPerson
ni nada por el estilo en los archivos .class, código que lo ve más adelante (con una reflexión, o si lo saca de otro la colección donde se ha convertido en Object
o así sucesivamente) no puede decir de ninguna manera que se supone que es una lista que contiene solo Person
y no solo cualquier otra lista de matriz.
Las plantillas de C ++ te permiten declarar algo como esto
std::list<Person>* foo = new std::list<Person>();
Se parece a los genéricos de C # y Java, y hará lo que creas que debería hacer, pero detrás de la escena suceden cosas diferentes.
Tiene la mayoría en común con los genéricos de C #, ya que crea pseudo-classes
especiales pseudo-classes
lugar de simplemente tirar la información tipográfica como lo hace Java, pero es un hervidor de peces completamente diferente.
Tanto C # como Java producen resultados diseñados para máquinas virtuales. Si escribe algún código que contenga una clase de Person
, en ambos casos, cierta información sobre una clase de Person
entrará en el archivo .dll o .class, y la JVM / CLR hará cosas con esto.
C ++ produce código binario x86 en bruto. No todo es un objeto, y no hay una máquina virtual subyacente que deba conocer una clase de Person
. No hay boxeo ni unboxing, y las funciones no tienen que pertenecer a clases, ni a nada.
Debido a esto, el compilador de C ++ no impone restricciones sobre lo que puede hacer con las plantillas; básicamente, cualquier código que pueda escribir manualmente, puede hacer que las plantillas escriban para usted.
El ejemplo más obvio es agregar cosas:
En C # y Java, el sistema de genéricos necesita saber qué métodos están disponibles para una clase y debe pasar esto a la máquina virtual. La única forma de decirlo es mediante la codificación rígida de la clase real o mediante interfaces. Por ejemplo:
string addNames<T>( T first, T second ) { return first.Name() + second.Name(); }
Ese código no se compilará en C # o Java, porque no sabe que el tipo T
realidad proporciona un método llamado Nombre (). Tienes que decirlo, en C # así:
interface IHasName{ string Name(); };
string addNames<T>( T first, T second ) where T : IHasName { .... }
Y luego debes asegurarte de que las cosas que pasas para agregarNombres implementan la interfaz IHasName y así sucesivamente. La sintaxis de Java es diferente ( <T extends IHasName>
), pero tiene los mismos problemas.
El caso ''clásico'' para este problema es tratar de escribir una función que haga esto
string addNames<T>( T first, T second ) { return first + second; }
Realmente no puede escribir este código porque no hay formas de declarar una interfaz con el método +
en él. Fallaste.
C ++ no sufre ninguno de estos problemas. Al compilador no le importa pasar los tipos a cualquier VM: si ambos objetos tienen una función .Name (), se compilará. Si no lo hacen, no lo hará. Sencillo.
Entonces, ahí lo tienen :-)