¿Cuáles son las diferencias entre los genéricos en C# y Java… y las plantillas en C++?[cerrado]

StackOverflow https://stackoverflow.com/questions/31693

Pregunta

Utilizo 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 genéricos?¿Pros/contras de cada uno?

¿Fue útil?

Solución

Agregaré mi voz al ruido e intentaré aclarar las cosas:

Los genéricos de C# le permiten declarar algo como esto.

List<Person> foo = new List<Person>();

y luego el compilador evitará que coloques cosas que no están Person en la lista.
Detrás de escena, el compilador de C# está poniendo List<Person> en el archivo .NET dll, pero en tiempo de ejecución el compilador JIT construye un nuevo conjunto de código, como si hubiera escrito una clase de lista especial solo para contener personas, algo así como ListOfPerson.

El beneficio de esto es que lo hace realmente rápido.No hay casting ni ninguna otra cosa, y debido a que el dll contiene la información de que se trata de una Lista de Person, otro código que lo analiza más adelante usando la reflexión puede decir que contiene Person objetos (para que obtengas intellisense, etc.).

La desventaja de esto es que el código antiguo de C# 1.0 y 1.1 (antes de que agregaran los genéricos) no comprende estos nuevos List<something>, por lo que tienes que convertir manualmente las cosas a la versión anterior. List para interoperar con ellos.Esto 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.

Los genéricos de Java le permiten declarar algo como esto.

ArrayList<Person> foo = new ArrayList<Person>();

En la superficie parece lo mismo, y más o menos lo es.El compilador también evitará que coloques 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 construye un programa especial. ListOfPerson - solo usa el viejo y simple ArrayList que siempre ha estado en Java.Cuando sacas cosas de la matriz, lo habitual Person p = (Person)foo.get(1); El casting de baile todavía está por hacer.El compilador le ahorra las pulsaciones de teclas, pero el golpe/lanzamiento de velocidad aún se produce como siempre.
Cuando la gente menciona "Borrado de tipo", esto es de lo que están hablando.El compilador inserta las conversiones por usted y luego "borra" el hecho de que debe ser una lista de Person No solo Object

El beneficio de este enfoque es que el código antiguo que no comprende los genéricos no tiene por qué preocuparse.Sigue lidiando con lo mismo de siempre. ArrayList como siempre lo ha hecho.Esto es más importante en el mundo de Java porque querían admitir la compilación de código usando Java 5 con genéricos y ejecutarlo en la versión 1.4 anterior o en JVM anteriores, con lo que Microsoft decidió deliberadamente no molestarse.

La desventaja es el golpe de velocidad que mencioné anteriormente, y también porque no hay ListOfPerson pseudoclase o algo así en los archivos .class, código que lo analiza más adelante (con reflexión, o si lo saca de otra colección donde se convirtió en Object o así sucesivamente) no puedo decir de ninguna manera que debe ser una lista que contenga solo Person y no cualquier otra lista de matrices.

Las plantillas de C++ te permiten declarar algo como esto

std::list<Person>* foo = new std::list<Person>();

Parece genérico de C# y Java, y hará lo que usted cree que debería hacer, pero detrás de escena están sucediendo cosas diferentes.

Tiene más en común con los genéricos de C# en el sentido de que crea pseudo-classes en lugar de simplemente tirar la información de tipo como lo hace Java, pero es un asunto completamente diferente.

Tanto C# como Java producen resultados diseñados para máquinas virtuales.Si escribes algún código que tenga un Person clase en él, en ambos casos alguna información sobre un Person class irá al archivo .dll o .class, y JVM/CLR hará cosas con esto.

C++ produce código binario x86 sin formato.Todo es no un objeto, y no hay ninguna máquina virtual subyacente que necesite saber acerca de un 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 las escriban 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 debe pasarlos a la máquina virtual.La única forma de saber esto 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 en 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 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 sufre los mismos problemas.

El caso "clásico" de este problema es intentar escribir una función que haga esto

string addNames<T>( T first, T second ) { return first + second; }

En realidad, no puedes 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á.Simple.

Pues ahí lo tienes :-)

Otros consejos

C++ rara vez utiliza la terminología "genérica".En cambio, se utiliza la palabra "plantillas" 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 tanto C# como Java por dos razones principales.La primera razón es que las plantillas de C++ no solo permiten argumentos de tipo en tiempo de compilación, sino también argumentos de valor constante en tiempo de compilación:las plantillas se pueden dar como números enteros o incluso firmas de funciones.Esto significa que puedes hacer algunas cosas bastante originales 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 distintiva 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 utiliza siempre que el argumento se evalúa como 1.Esto me permite definir una recursividad 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, no todos los tipos pueden tratarse por igual dentro de la implementación.Las plantillas de C++ tienen esto en cuenta.Esta es prácticamente la misma diferencia que hace la programación orientada a objetos entre interfaz e implementación con la anulación de 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 que están definidos por dos iteradores, que apuntan al principio y detrás del final del contenedor.Por tanto, todo el contenido está circunscrito por los iteradores:comienzo <= elementos <fin.

Usar iteradores en lugar de contenedores es útil porque permite operar en partes de un contenedor en lugar de hacerlo en su totalidad.

Otra característica distintiva de C++ es la posibilidad de especialización parcial para plantillas de clases.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 de manera más eficiente que otros tipos aplicando algún truco especial.Podemos hacer esto por parcialmente especializado en todo tipo de punteros:

template <typename T>
class Store<T*> { … }; // (2)

Ahora, cada vez que instanciamos una plantilla de contenedor para un tipo, se utiliza la definición adecuada:

Store<int> x; // Uses (1)
Store<int*> y; // Uses (2)
Store<string**> z; // Uses (2), with T = string*.

El propio Anders Hejlsberg describió aquí las diferencias "Genéricos en C#, Java y C++".

Ya hay muchas buenas respuestas sobre qué las diferencias son, así que déjame darte una perspectiva ligeramente diferente y agregar el por qué.

Como ya se explicó, la principal diferencia es tipo de borrado, 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 no?

Bueno, ¿cuál es la alternativa?Si no implementa genéricos en el idioma, donde hacer ¿los implementas?Y la respuesta es:en la Máquina Virtual.Lo que rompe la compatibilidad con versiones anteriores.

El borrado de tipos, 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 aún se puede implementar en Java 1.4.

Microsoft, sin embargo, decidió romper la compatibilidad con versiones anteriores de los genéricos. Eso es por qué los genéricos .NET son "mejores" que los genéricos de Java.

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 Idioma (lo que significa que se aplican solo a Java, no a otros lenguajes), en .NET son parte del Máquina virtual (lo que significa que se aplican a todo lenguajes, no sólo C# y Visual Basic.NET).

Compare esto con características de .NET como LINQ, expresiones lambda, inferencia de tipos de variables locales, tipos anónimos y árboles de expresión:estos son todos idioma características.Es por eso que existen diferencias sutiles entre VB.NET y C#:Si esas características fueran parte de la VM, serían las mismas en todo 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 así ejecutarlo en .NET 2.0, siempre que no utilice ninguna biblioteca .NET 3.5.Eso podría no funciona con genéricos y .NET 1.1, pero haría trabajar con Java y Java 1.4.

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 las plantillas, el IDE nunca puede estar realmente seguro de si un miembro determinado 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 punto si, y qué, miembros a tiene.Para otros lenguajes, el análisis sería sencillo, pero para C++, se necesita bastante evaluación de antemano.

Se pone peor.Y si my_int_type ¿También se definieron 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 lo tanto b debe ser del mismo tipo que a, ¿bien?

Equivocado.En el momento en que el compilador intenta resolver esta declaración, en realidad no sabe Y<int>::my_type ¡todavía!Por lo tanto, no sabe que se trata de un tipo.Podría ser otra cosa, p.una función miembro o un campo.Esto podría dar lugar a ambigüedades (aunque no en el presente caso), por lo que 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 ambigüedades a partir 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 para 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, por lo que tenemos que eliminar la ambigüedad aquí.

Tanto Java como C# introdujeron genéricos después del lanzamiento de su primer lenguaje.Sin embargo, existen diferencias en cómo cambiaron las bibliotecas principales cuando se introdujeron los genéricos. Los genéricos de C# no son sólo magia del compilador y por eso no fue posible generar clases de biblioteca existentes sin romper la compatibilidad con versiones anteriores.

Por ejemplo, en Java el existente Marco de colecciones era completamente genérico. Java no tiene una versión genérica y no genérica heredada 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 optar por la versión no genérica, pero esas clases heredadas permanecen en su lugar, saturando el panorama.

Otra diferencia notable son las clases Enum en Java y C#. Enum de Java tiene esta definición que parece algo tortuosa:

//  java.lang.Enum Definition in Java
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {

(ver el muy claro comentario de Angelika Langer explicación de exactamente por qué Es tan.Básicamente, esto significa que Java puede dar acceso seguro a tipos 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.

Con 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 supongamos 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 su lugar, puedes escribir:

public void Foo(Collection<?> thing)

El signo de interrogación le pide al compilador que pretenda 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 no pueda hacer también con un parámetro de tipo con nombre (que es como siempre se hacen estas cosas en C++ y C#).

Wikipedia tiene excelentes artículos que comparan ambos. Genéricos de Java/C# y Genéricos de Java/C++ plantillas.El artículo principal sobre genéricos Parece un poco desordenado pero tiene buena información.

La mayor queja es el borrado de tipos.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 tipo de borrado: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 tiempo de compilación y admiten la especialización.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 puedes calcular cualquier cosa que sea computable con una máquina de Turing).

En Java, los genéricos son sólo a 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 matrices, no una lista de cadenas.Entonces, el tipo de 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 los genéricos y romper la compatibilidad con versiones anteriores:

Actualmente, los genéricos se implementan utilizando Erasure, lo que significa que la información de tipo genérico no está disponible en tiempo de ejecución, lo que hace que algún tipo de código sea difícil de escribir.Los genéricos se implementaron de esta manera para admitir la compatibilidad hacia atrás con un código no genérico anterior.Reified Generics pondría la información de tipo genérico disponible en el tiempo de ejecución, lo que rompería el código no genérico heredado.Sin embargo, Neal Gafter ha propuesto hacer que los tipos sean reificables solo si se especifican, para no romper la compatibilidad con respecto.

en Artículo de Alex Miller sobre las propuestas de Java 7

NÓTESE BIEN:No tengo suficiente punto para comentar, así que siéntete libre de mover esto como comentario a la respuesta adecuada.

Contrariamente a la creencia popular, que nunca entiendo de dónde vino, .net implementó verdaderos genéricos sin romper la compatibilidad con versiones anteriores, y dedicaron un esfuerzo explícito a ello.No es necesario que cambie su código .net 1.0 no genérico a genérico solo para usarlo en .net 2.0.Tanto las listas genéricas como las no genéricas todavía están disponibles en .Net framework 2.0 incluso hasta 4.0, exactamente 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 ArrayList que antes.La compatibilidad con el código anterior siempre se mantiene desde 1.0 hasta ahora...Entonces, incluso en .net 4.0, aún tiene la opción de usar cualquier clase no genérica de 1.0 BCL si así lo desea.

Así que no creo que Java tenga que romper la compatibilidad con versiones anteriores para admitir verdaderos genéricos.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top