Quais são as diferenças entre Generics em C#e Java… e Templates em C++?




(9)

11 meses atrasado, mas acho que esta pergunta está pronta para algumas coisas de Java Wildcard.

Este é um recurso sintático do Java. Suponha que você tenha um método:

public <T> void Foo(Collection<T> thing)

E suponha que você não precise se referir ao tipo T no corpo do método. Você está declarando um nome T e, em seguida, usando apenas uma vez, então por que você deve pensar em um nome para ele? Em vez disso, você pode escrever:

public void Foo(Collection<?> thing)

O ponto de interrogação pede ao compilador para fingir que você declarou um parâmetro de tipo nomeado normal que só precisa aparecer uma vez naquele ponto.

Não há nada que você possa fazer com curingas que você não pode fazer com um parâmetro de tipo nomeado (que é como essas coisas são sempre feitas em C ++ e C #).

Eu principalmente uso Java e genéricos são relativamente novos. Eu continuo lendo que o Java tomou a decisão errada ou que o .NET tem implementações melhores etc. etc.

Então, quais são as principais diferenças entre C ++, C #, Java em genéricos? Prós / contras de cada um?



Acompanhamento da minha postagem anterior.

Modelos são uma das principais razões pelas quais o C ++ falha de forma tão profunda no intellisense, independentemente do IDE usado. Devido à especialização de modelo, o IDE nunca pode ter certeza se um determinado membro existe ou não. Considerar:

template <typename T>
struct X {
    void foo() { }
};

template <>
struct X<int> { };

typedef int my_int_type;

X<my_int_type> a;
a.|

Agora, o cursor está na posição indicada e é muito difícil para o IDE dizer nesse ponto se, e o que, os membros a possuem. Para outras linguagens, a análise seria simples, mas para C ++, é necessário um pouco de avaliação prévia.

Fica pior. E se my_int_type fosse definido dentro de um modelo de classe? Agora seu tipo dependeria de outro argumento de tipo. E aqui, até os compiladores falham.

template <typename T>
struct Y {
    typedef T my_type;
};

X<Y<int>::my_type> b;

Depois de pensar um pouco, um programador concluiria que esse código é o mesmo que o acima: Y<int>::my_type resolve para int , portanto, b deve ser do mesmo tipo que a , certo?

Errado. No ponto em que o compilador tenta resolver essa instrução, ela ainda não sabe Y<int>::my_type ainda! Portanto, não sabe que isso é um tipo. Pode ser outra coisa, por exemplo, uma função de membro ou um campo. Isso pode dar origem a ambiguidades (embora não no presente caso), portanto, o compilador falha. Temos que dizer explicitamente que nos referimos a um nome de tipo:

X<typename Y<int>::my_type> b;

Agora, o código compila. Para ver como as ambigüidades surgem dessa situação, considere o seguinte código:

Y<int>::my_type(123);

Essa instrução de código é perfeitamente válida e informa ao C ++ para executar a chamada de função para Y<int>::my_type . No entanto, se my_type não for uma função, mas sim um tipo, essa instrução ainda será válida e executará uma my_type especial (a conversão de estilo de função) que geralmente é uma chamada de construtor. O compilador não pode dizer o que queremos dizer, então temos que desambiguar aqui.


Em Java, os genéricos são apenas de nível de compilador, então você obtém:

a = new ArrayList<String>()
a.getClass() => ArrayList

Note que o tipo de 'a' é uma lista de arrays, não uma lista de strings. Portanto, o tipo de lista de bananas seria igual () a uma lista de macacos.

Por assim dizer.


Já existem muitas respostas boas sobre quais são as diferenças, então deixe-me dar uma perspectiva ligeiramente diferente e adicionar o porquê .

Como já foi explicado, a principal diferença é o apagamento de tipos , isto é, o fato de que o compilador Java apaga os tipos genéricos e eles não acabam no bytecode gerado. No entanto, a questão é: por que alguém faria isso? Não faz sentido! Ou isso?

Bem, qual a alternativa? Se você não implementa genéricos na linguagem, onde você os implementa? E a resposta é: na máquina virtual. Que quebra a compatibilidade retroativa.

A eliminação de tipos, por outro lado, permite misturar clientes genéricos com bibliotecas não genéricas. Em outras palavras: o código que foi compilado no Java 5 ainda pode ser implementado no Java 1.4.

A Microsoft, no entanto, decidiu quebrar a compatibilidade retroativa para genéricos. É por isso que os .NET Generics são "melhores" que os Java Generics.

Claro, a Sun não é idiota ou covarde. A razão pela qual eles "fracassaram" foi que o Java era significativamente mais antigo e mais difundido do que o .NET quando introduziam genéricos. (Eles foram introduzidos mais ou menos ao mesmo tempo em ambos os mundos.) Quebrar a compatibilidade retroativa teria sido uma grande dor.

Dito de outra forma: em Java, os Genéricos fazem parte da Linguagem (o que significa que eles se aplicam apenas ao Java, não a outras linguagens), no .NET fazem parte da Máquina Virtual (o que significa que se aplicam a todas as linguagens, não apenas C # e Visual Basic.NET).

Compare isso com recursos .NET como LINQ, expressões lambda, inferência de tipo de variável local, tipos anônimos e árvores de expressão: todos esses recursos de linguagem . É por isso que há diferenças sutis entre o VB.NET e o C #: se esses recursos fizessem parte da VM, eles seriam os mesmos em todos os idiomas. Mas o CLR não mudou: continua o mesmo no .NET 3.5 SP1 e no .NET 2.0. Você pode compilar um programa C # que use LINQ com o compilador .NET 3.5 e ainda executá-lo no .NET 2.0, desde que você não use nenhuma biblioteca .NET 3.5. Isso não funcionaria com genéricos e .NET 1.1, mas funcionaria com Java e Java 1.4.


Modelos C ++ são realmente muito mais poderosos que seus correspondentes C # e Java, pois são avaliados em tempo de compilação e suporte a especialização. Isso permite a meta-programação de modelo e torna o compilador C ++ equivalente a uma máquina de Turing (ou seja, durante o processo de compilação você pode calcular qualquer coisa que seja computável com uma máquina de Turing).


O C ++ raramente usa a terminologia "genérica". Em vez disso, a palavra "templates" é usada e é mais precisa. Modelos descreve uma técnica para obter um design genérico.

Modelos C ++ são muito diferentes do que o C # e o Java implementam por dois motivos principais. A primeira razão é que os modelos C ++ não permitem apenas argumentos de tipo de tempo de compilação, mas também argumentos de valores constantes de compilação: os modelos podem ser dados como inteiros ou até mesmo assinaturas de função. Isso significa que você pode fazer algumas coisas bem funky em tempo de compilação, por exemplo, 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;

Esse código também usa o outro recurso distinto dos modelos C ++, ou seja, a especialização de modelo. O código define um modelo de classe, product que possui um argumento de valor. Ele também define uma especialização para esse modelo que é usado sempre que o argumento é avaliado como 1. Isso permite que eu defina uma recursão sobre definições de modelo. Eu acredito que isso foi descoberto pela primeira vez por Andrei Alexandrescu .

A especialização de modelo é importante para o C ++ porque permite diferenças estruturais nas estruturas de dados. Modelos como um todo são um meio de unificar uma interface entre tipos. No entanto, embora isso seja desejável, todos os tipos não podem ser tratados igualmente dentro da implementação. Modelos C ++ levam isso em conta. Essa é a mesma diferença que a OOP faz entre a interface e a implementação com a substituição dos métodos virtuais.

Modelos C ++ são essenciais para seu paradigma de programação algorítmica. Por exemplo, quase todos os algoritmos para contêineres são definidos como funções que aceitam o tipo de contêiner como um tipo de modelo e os tratam uniformemente. Na verdade, isso não está certo: o C ++ não funciona em contêineres, mas em intervalos que são definidos por dois iteradores, apontando para o início e para o final do contêiner. Assim, todo o conteúdo é circunscrito pelos iteradores: begin <= elements <end.

Usar iteradores em vez de contêineres é útil porque permite operar em partes de um contêiner em vez de no todo.

Outra característica distintiva do C ++ é a possibilidade de especialização parcial para modelos de classes. Isso é um pouco relacionado à correspondência de padrões em argumentos em Haskell e outras linguagens funcionais. Por exemplo, vamos considerar uma classe que armazena elementos:

template <typename T>
class Store {  }; // (1)

Isso funciona para qualquer tipo de elemento. Mas digamos que podemos armazenar os indicadores de maneira mais eficiente que outros tipos, aplicando algum truque especial. Podemos fazer isso parcialmente especializado para todos os tipos de ponteiro:

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

Agora, sempre que instanciamos um modelo de contêiner para um tipo, a definição apropriada é usada:

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


Tanto o Java quanto o C # introduziram genéricos após o lançamento do primeiro idioma. No entanto, existem diferenças na forma como as bibliotecas principais mudaram quando os genéricos foram introduzidos. Os genéricos do C # não são apenas mágicos do compilador e, portanto, não era possível generalizar classes de bibliotecas existentes sem quebrar a compatibilidade retroativa.

Por exemplo, em Java, o Framework de Coleções existente foi completamente genérico . Java não possui uma versão genérica e não genérica legada das classes de coleções. De certa forma isso é muito mais limpo - se você precisa usar uma coleção em C #, há muito poucos motivos para usar a versão não genérica, mas essas classes de legado permanecem no lugar, bagunçando a paisagem.

Outra diferença notável é as classes Enum em Java e C #. O Enum do Java tem essa definição de aparência um tanto tortuosa:

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

(Veja a explicação muito clara de Angelika Langer sobre exatamente por que isso acontece. Essencialmente, isso significa que o Java pode dar acesso seguro a um tipo de string a seu valor Enum:

//  Parsing String to Enum in Java
Colour colour = Colour.valueOf("RED");

Compare isso com a versão do C #:

//  Parsing String to Enum in C#
Colour colour = (Colour)Enum.Parse(typeof(Colour), "RED");

Como o Enum já existia em C # antes dos genéricos serem introduzidos na linguagem, a definição não poderia mudar sem quebrar o código existente. Assim, como coleções, permanece nas bibliotecas principais neste estado legado.





templates