Respuestas:
Agregaré mi voz al ruido y trataré de aclarar las cosas:
List<Person> foo = new List<Person>();
y luego el compilador le impedirá poner cosas que no están Person
en la lista.
Detrás de escena, el compilador de C # solo se está poniendo List<Person>
en el archivo .NET dll, pero en tiempo de ejecución el compilador JIT va y crea un nuevo conjunto de código, como si hubiera escrito una clase de lista especial solo para contener personas, algo así ListOfPerson
.
El beneficio de esto es que lo hace realmente rápido. No hay conversión ni ninguna otra cosa, y debido a que el dll contiene la información de la que es una Lista Person
, otro código que lo mira más adelante usando la reflexión puede decir que contiene Person
objetos (por lo que obtienes intellisense, etc.).
La desventaja de esto es que el viejo código C # 1.0 y 1.1 (antes de que agregaran genéricos) no comprende estos nuevos List<something>
, por lo que debe convertir manualmente las cosas nuevamente a viejas List
para interactuar con ellos. Este no es un gran problema, 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
ArrayList<Person> foo = new ArrayList<Person>();
En la superficie se ve igual, y más o menos lo es. El compilador también le impedirá poner cosas que no están Person
en la lista.
La diferencia es lo que sucede detrás de escena. A diferencia de C #, Java no va y crea un especial ListOfPerson
, solo usa el viejo y simple ArrayList
que siempre ha estado en Java. Cuando saca las cosas de la matriz, Person p = (Person)foo.get(1);
todavía tiene que hacerse el baile de casting habitual . El compilador le está ahorrando las pulsaciones de teclas, pero la velocidad de golpe / lanzamiento aún se produce como siempre.
Cuando las personas mencionan "Type Erasure", esto es de lo que están hablando. El compilador inserta los moldes para usted y luego 'borra' el hecho de que está destinado a ser una lista Person
no soloObject
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 lo mismo de ArrayList
siempre. Esto es más importante en el mundo de Java porque querían admitir la compilación de código utilizando Java 5 con genéricos y ejecutarlo en versiones anteriores de 1.4 o JVM anteriores, que Microsoft decidió deliberadamente no molestarse.
La desventaja es el golpe de velocidad que mencioné anteriormente, y también porque no hay ListOfPerson
pseudo-clase ni nada de eso en los archivos .class, código que lo mira más adelante (con reflexión, o si lo sacas de otra colección) donde se ha convertido, Object
etc.) no puede decir de ninguna manera que está destinado a ser una lista que contiene solo Person
y no cualquier otra lista de matriz.
std::list<Person>* foo = new std::list<Person>();
Parece genéricos C # y Java, y hará lo que crees que debería hacer, pero detrás de escena están sucediendo cosas diferentes.
Tiene mucho más en común con los genéricos de C #, ya que crea elementos especiales en pseudo-classes
lugar de simplemente desechar la información de tipo como lo hace Java, pero es una caldera de peces completamente diferente.
Tanto C # como Java producen resultados diseñados para máquinas virtuales. Si escribe algún código que tenga una Person
clase, en ambos casos alguna información sobre una Person
clase irá al archivo .dll o .class, y la JVM / CLR hará cosas con esto.
C ++ produce código binario x86 sin formato. Todo no es un objeto, y no hay una máquina virtual subyacente que deba saber sobre una Person
clase. No hay boxing 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 obtener plantillas para que escriba por usted.
El ejemplo más obvio es agregar cosas:
En C # y Java, el sistema genérico necesita saber qué métodos están disponibles para una clase, y necesita pasar esto a la máquina virtual. La única forma de saberlo es codificando la clase real o usando 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
realmente proporciona un método llamado Name (). Tienes que decirlo, en C # así:
interface IHasName{ string Name(); };
string addNames<T>( T first, T second ) where T : IHasName { .... }
Y luego debe asegurarse de que las cosas que pasa a addNames implementen 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 intentar escribir una función que haga esto
string addNames<T>( T first, T second ) { return first + second; }
En realidad, 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 tipos a ninguna máquina virtual; si ambos objetos tienen una función .Name (), se compilará. Si no lo hacen, no lo hará. Sencillo.
Entonces, ahí lo tienes :-)
int addNames<T>( T first, T second ) { return first + second; }
en C #. El tipo genérico se puede restringir a una clase en lugar de una interfaz, y hay una manera de declarar una clase con el +
operador en ella.
C ++ rara vez utiliza la terminología "genérica". En cambio, se usa la palabra "plantillas" y es más precisa. Plantillas describe 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 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 firmas de funciones. Esto significa que puede hacer algunas cosas bastante funky 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 usa 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 cada vez que el argumento se evalúa como 1. Esto me permite definir una recursión sobre las definiciones de plantilla. Creo que esto fue descubierto 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 ser tratados 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 la anulación 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 está del todo bien: 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.
El uso de iteradores en lugar de contenedores es útil porque permite operar en partes de un contenedor en lugar de en su conjunto.
Otra característica distintiva de C ++ es la posibilidad de especialización parcial para plantillas de clase. Esto está algo relacionado con la coincidencia de patrones en 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 hacer esto especializándonos parcialmente para todos los tipos de puntero:
template <typename T>
class Store<T*> { … }; // (2)
Ahora, cada vez que instanciamos 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*.
Anders Hejlsberg mismo describió las diferencias aquí " Genéricos en C #, Java y C ++ ".
Ya hay muchas buenas respuestas 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 la eliminación de tipos , es decir, el hecho de que el compilador de Java borra los tipos genéricos y no terminan en el código de bytes generado. Sin embargo, la pregunta es: ¿por qué alguien haría eso? ¡No tiene sentido! O lo hace?
Bueno, cual es la alternativa? Si no implementa genéricos en el idioma, ¿ dónde los implementa? Y la respuesta es: en la máquina virtual. Lo que rompe la compatibilidad con versiones anteriores.
El borrado de tipo, por otro lado, le permite mezclar clientes genéricos con bibliotecas no genéricas. En otras palabras: el código que se compiló en Java 5 todavía se puede implementar en Java 1.4.
Microsoft, sin embargo, decidió romper la compatibilidad con versiones anteriores para los genéricos. Es por eso que .NET Generics es "mejor" que Java Generics.
Por supuesto, Sun no son idiotas ni cobardes. La razón por la que se "acobardaron" fue que Java era significativamente más antiguo y estaba más extendido que .NET cuando introdujeron los genéricos. (Se introdujeron aproximadamente al mismo tiempo en ambos mundos). Romper la compatibilidad con versiones anteriores habría 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 las características de .NET como LINQ, expresiones lambda, inferencia de tipos de variables locales, tipos anónimos y árboles de expresiones: estas son todas las características del lenguaje . 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 C # que use LINQ con el compilador .NET 3.5 y aún ejecutarlo en .NET 2.0, siempre que no use ninguna biblioteca .NET 3.5. Eso no trabajo con los genéricos y .NET 1.1, pero sería trabajar con Java y Java 1.4.
ArrayList<T>
se puede emitir como un nuevo tipo con nombre interno con un Class<T>
campo estático (oculto) . Siempre que la nueva versión de lib genérica se haya implementado con el código de byte 1.5+, podrá ejecutarse en 1.4 JVM.
Seguimiento de mi publicación anterior.
Las plantillas son una de las principales razones 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 para el IDE decir en ese momento si, y qué, tienen los miembros a
. Para otros lenguajes, el análisis sería sencillo, pero para C ++, se necesita bastante evaluación de antemano.
Se pone peor. ¿Qué pasaría si también my_int_type
se definiera dentro de una plantilla de clase? Ahora su tipo dependería de otro argumento de tipo. 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 concluiría que este código es el mismo que el anterior: Y<int>::my_type
resuelve int
, por b
lo tanto, debe ser del mismo tipo que a
, ¿verdad?
Incorrecto. En el punto donde el compilador intenta resolver esta declaración, ¡aún no lo sabe 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 puede generar 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 se 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 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 conversión especial (la conversión de estilo de función) que a menudo es una invocación de constructor. El compilador no puede decir a qué nos referimos, así que tenemos que desambiguar aquí.
Tanto Java como C # introdujeron genéricos después de su lanzamiento en el primer idioma. Sin embargo, existen diferencias en cómo cambiaron las bibliotecas principales cuando se introdujeron los genéricos. Los genéricos de C # no son solo magia de compilación, por lo que no fue posible generar clases de biblioteca existentes sin romper la compatibilidad con versiones anteriores.
Por ejemplo, en Java, el Framework de Colecciones existente estaba completamente genérico . Java no tiene una versión genérica y una versión no genérica heredada de las clases de colecciones. De alguna manera, esto es mucho más limpio: si necesita usar una colección en C #, realmente hay muy pocas razones para usar la versión no genérica, pero esas clases heredadas permanecen en su lugar, abarrotando el paisaje.
Otra diferencia notable son las clases Enum en Java y C #. Enum de Java tiene esta definición de aspecto algo tortuosa:
// 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 exactamente por qué esto es así. Esencialmente, esto significa que Java puede dar acceso seguro de tipo 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 principales en este estado heredado.
ArrayList
de List<T>
y ponerlo en un nuevo espacio de nombres. El hecho es que si apareciera una clase en el código fuente, ya ArrayList<T>
que se convertirá en un nombre de clase generado por el compilador diferente en el código IL, entonces no puede haber conflictos de nombres.
11 meses de retraso, pero creo que esta pregunta está lista para algunas cosas de Java Wildcard.
Esta es una característica sintáctica de Java. Supongamos que tiene un método:
public <T> void Foo(Collection<T> thing)
Y suponga que no necesita hacer referencia al tipo T en el cuerpo del método. Estás declarando un nombre T y luego solo lo usas una vez, entonces ¿por qué deberías pensar en un nombre para él? En cambio, puedes escribir:
public void Foo(Collection<?> thing)
El signo de interrogación le pide al compilador que finja 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 comodines que tampoco pueda hacer con un parámetro de tipo con nombre (que es cómo se hacen siempre estas cosas en C ++ y C #).
class Foo<T extends List<?>>
y usar, Foo<StringList>
pero en C # debe agregar ese parámetro de tipo adicional: class Foo<T, T2> where T : IList<T2>
y usar el torpe Foo<StringList, String>
.
Wikipedia tiene grandes escribir-ups que comparan ambos Java / C # genéricos y Java genéricos / C ++ plantillas. El artículo principal sobre Genéricos parece un poco desordenado pero tiene buena información.
La mayor queja es el tipo de borrado. 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 por 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 C ++ son en realidad mucho más potentes que sus contrapartes C # y Java, ya que se evalúan en tiempo de compilación y se especializan en soporte. Esto permite la metaprogramació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 cualquier cosa que sea computable con una máquina de Turing).
En Java, los genéricos son solo de nivel de compilador, por lo 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 plátanos sería igual a () una lista de monos.
Por así decirlo.
Parece que, entre otras propuestas muy interesantes, hay una sobre refinar genéricos y romper la compatibilidad con versiones anteriores:
Actualmente, los genéricos se implementan mediante 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 códigos anteriores no genéricos. Los genéricos reificados pondrían a disposición la información del tipo genérico en tiempo de ejecución, lo que rompería el código heredado no genérico. Sin embargo, Neal Gafter ha propuesto hacer que los tipos sean reifiable solo si se especifica, para no romper la compatibilidad con versiones anteriores.
en el artículo de Alex Miller sobre las propuestas de Java 7
NB: no tengo suficiente punto para comentar, así que siéntase libre de mover esto como un comentario para responder adecuadamente.
Contrariamente a la creencia popular, que nunca entiendo de dónde vino, .net implementó genéricos verdaderos 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 usarlo en .net 2.0. Las listas genéricas y no genéricas todavía están disponibles en .Net framework 2.0 incluso 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 usan la misma clase ArrayList que antes. La compatibilidad con el código hacia atrás siempre se mantiene desde 1.0 hasta ahora ... Por lo tanto, incluso en .net 4.0, todavía tiene la opción de usar cualquier clase no genérica de 1.0 BCL si elige hacerlo.
Por lo tanto, no creo que Java tenga que romper la compatibilidad con versiones anteriores para admitir genéricos verdaderos.
ArrayList<Foo>
que quiere pasar a un método anterior que supuestamente debe llenar ArrayList
con instancias de Foo
. Si un ArrayList<foo>
no es un ArrayList
, ¿cómo se hace que eso funcione?