Pergunta

Eu uso principalmente Java e os genéricos são relativamente novos.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?

Foi útil?

Solução

Vou adicionar minha voz ao barulho e tentar deixar as coisas claras:

Os genéricos do C# permitem que você declare algo assim.

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

e então o compilador impedirá que você coloque coisas que não são Person na lista.
Nos bastidores, o compilador C# está apenas colocando List<Person> no arquivo .NET dll, mas em tempo de execução o compilador JIT cria um novo conjunto de código, como se você tivesse escrito uma classe de lista especial apenas para conter pessoas - algo como ListOfPerson.

A vantagem disso é que torna tudo muito rápido.Não há casting ou qualquer outra coisa, e como a dll contém a informação de que esta é uma lista de Person, outro código que o analisa posteriormente usando reflexão pode dizer que ele contém Person objetos (para obter intellisense e assim por diante).

A desvantagem disso é que o antigo código C# 1.0 e 1.1 (antes de adicionarem os genéricos) não entende esses novos List<something>, então você tem que converter manualmente as coisas de volta ao antigo List para interoperar com eles.Isso não é um grande problema, porque o código binário C# 2.0 não é compatível com versões anteriores.A única vez que isso acontecerá é se você estiver atualizando algum código C# 1.0/1.1 antigo para C# 2.0

Java Generics permitem que você declare algo assim.

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

Superficialmente, parece o mesmo, e de certa forma é.O compilador também impedirá que você coloque coisas que não são Person na lista.

A diferença é o que acontece nos bastidores.Ao contrário do C#, Java não cria um ListOfPerson - ele apenas usa o velho ArrayList que sempre esteve em Java.Quando você tira coisas do array, o normal Person p = (Person)foo.get(1); a dança do elenco ainda precisa ser feita.O compilador está economizando o pressionamento de teclas, mas a velocidade de acerto/lançamento ainda ocorre como sempre foi.
Quando as pessoas mencionam "Type Erasure", é disso que estão falando.O compilador insere as conversões para você e depois 'apaga' o fato de que deveria ser uma lista de Person não apenas Object

O benefício dessa abordagem é que o código antigo que não entende os genéricos não precisa se preocupar.Ainda está lidando com o mesmo velho ArrayList como sempre aconteceu.Isso é mais importante no mundo Java porque eles queriam suportar a compilação de código usando Java 5 com genéricos e executá-lo em JVMs 1.4 antigos ou anteriores, com os quais a Microsoft decidiu deliberadamente não se preocupar.

A desvantagem é o impacto na velocidade que mencionei anteriormente, e também porque não há ListOfPerson pseudoclasse ou algo parecido indo para os arquivos .class, código que analisa mais tarde (com reflexão, ou se você retirá-lo de outra coleção onde foi convertido em Object ou assim por diante) não posso dizer de forma alguma que se destina a ser uma lista contendo apenas Person e não qualquer outra lista de arrays.

Os modelos C++ permitem que você declare algo assim

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

Parece genéricos de C# e Java e fará o que você acha que deveria fazer, mas nos bastidores coisas diferentes estão acontecendo.

Ele tem mais em comum com os genéricos do C#, pois cria pseudo-classes em vez de apenas jogar fora as informações de tipo como o Java faz, mas é uma chaleira de peixes totalmente diferente.

Tanto C# quanto Java produzem resultados projetados para máquinas virtuais.Se você escrever algum código que tenha um Person classe nele, em ambos os casos algumas informações sobre um Person class irá para o arquivo .dll ou .class, e a JVM/CLR fará coisas com isso.

C++ produz código binário x86 bruto.Tudo é não um objeto, e não há nenhuma máquina virtual subjacente que precise saber sobre um Person aula.Não há boxing ou unboxing, e as funções não precisam pertencer a classes, nem mesmo a nada.

Por causa disso, o compilador C++ não impõe restrições sobre o que você pode fazer com modelos - basicamente qualquer código que você possa escrever manualmente, você pode obter modelos para escrever para você.
O exemplo mais óbvio é adicionar coisas:

Em C# e Java, o sistema genérico precisa saber quais métodos estão disponíveis para uma classe e precisa passar isso para a máquina virtual.A única maneira de saber isso é codificando a classe real ou usando interfaces.Por exemplo:

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

Esse código não será compilado em C# ou Java, porque não sabe que o tipo T na verdade, fornece um método chamado Name().Você tem que contar - em C# assim:

interface IHasName{ string Name(); };
string addNames<T>( T first, T second ) where T : IHasName { .... }

E então você precisa ter certeza de que as coisas que você passa para addNames implementam a interface IHasName e assim por diante.A sintaxe java é diferente (<T extends IHasName>), mas sofre dos mesmos problemas.

O caso 'clássico' para este problema é tentar escrever uma função que faça isso

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

Na verdade, você não pode escrever esse código porque não há maneiras de declarar uma interface com o + método nele.Você falhou.

C++ não sofre de nenhum desses problemas.O compilador não se importa em passar tipos para nenhuma VM - se ambos os objetos tiverem uma função .Name(), ele será compilado.Se não o fizerem, não acontecerá.Simples.

Então, aí está :-)

Outras dicas

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

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

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

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

Os 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 de maneira uniforme.Na verdade, isso não está certo:C++ não funciona em contêineres, mas sim em gamas que são definidos por dois iteradores, apontando para o início e atrás do final do contêiner.Assim, todo o conteúdo é circunscrito pelos iteradores:início <= elementos < fim.

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 classe.Isso está 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 possamos armazenar ponteiros com mais eficiência do que outros tipos, aplicando algum truque especial.Podemos fazer isso por parcialmente especializado em 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*.

O próprio Anders Hejlsberg descreveu as diferenças aqui "Genéricos em C#, Java e C++".

Já existem muitas boas respostas sobre o que as diferenças são, então deixe-me dar uma perspectiva um pouco diferente e adicionar o por que.

Como já foi explicado, a principal diferença é tipo apagamento, ou sejao fato de o compilador Java apagar os tipos genéricos e eles não acabarem no bytecode gerado.No entanto, a questão é:Por que alguém faria aquilo?Não faz sentido!Ou não?

Bem, qual é a alternativa?Se você não implementar genéricos na linguagem, onde fazer você os implementa?E a resposta é:na máquina virtual.O que quebra a compatibilidade com versões anteriores.

O apagamento de tipo, por outro lado, permite misturar clientes genéricos com bibliotecas não genéricas.Em outras palavras:o código que foi compilado em Java 5 ainda pode ser implementado em Java 1.4.

A Microsoft, no entanto, decidiu quebrar a retrocompatibilidade dos genéricos. Isso é por que os genéricos do .NET são "melhores" que os genéricos do Java.

Claro, Sun não são idiotas ou covardes.A razão pela qual eles "se acovardaram" foi que o Java era significativamente mais antigo e mais difundido que o .NET quando introduziram os genéricos.(Eles foram introduzidos aproximadamente ao mesmo tempo em ambos os mundos.) Quebrar a compatibilidade com versões anteriores teria sido uma grande dor.

Dito de outra forma:em Java, os genéricos fazem parte do Linguagem (o que significa que eles se aplicam apenas para Java, não para outras linguagens), no .NET eles fazem parte do Máquina virtual (o que significa que se aplicam a todos linguagens, não apenas C# e Visual Basic.NET).

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

Acompanhamento da minha postagem anterior.

Os modelos são uma das principais razões pelas quais o C++ falha tão profundamente no intellisense, independentemente do IDE usado.Devido à especialização do 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 quais membros a tem.Para outras linguagens, a análise seria simples, mas para C++, é necessária bastante avaliação prévia.

Fica pior.E se my_int_type foram definidos dentro de um modelo de classe também?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 este código é igual ao anterior: Y<int>::my_type resolve int, portanto b deve ser do mesmo tipo que a, certo?

Errado.No ponto em que o compilador tenta resolver esta instrução, ele na verdade não sabe Y<int>::my_type ainda!Portanto, ele não sabe que este é um tipo.Poderia ser outra coisa, por ex.uma função de membro ou um campo.Isto pode dar origem a ambigüidades (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 é compilado.Para ver como surgem as ambiguidades desta situação, considere o seguinte código:

Y<int>::my_type(123);

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

Tanto Java quanto C# introduziram genéricos após o lançamento da primeira linguagem.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ágica do compilador e por isso não foi possível gerar classes de biblioteca existentes sem quebrar a compatibilidade com versões anteriores.

Por exemplo, em Java, o existente Estrutura de coleções era completamente genérico. Java não possui uma versão genérica e uma versão herdada não genérica das classes de coleções. De certa forma, isso é muito mais limpo - se você precisar usar uma coleção em C#, há realmente poucos motivos para optar pela versão não genérica, mas essas classes herdadas permanecem no lugar, desordenando o cenário.

Outra diferença notável são as classes Enum em Java e C#. O Enum do Java tem esta definição um tanto tortuosa:

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

(veja Angelika Langer muito clara explicação de exatamente por que isto é tão.Essencialmente, isso significa que Java pode fornecer acesso seguro de tipo de uma string ao 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 Enum já existia em C# antes da introdução dos genéricos na linguagem, a definição não poderia ser alterada sem quebrar o código existente.Assim, como as coleções, ele permanece nas bibliotecas principais neste estado legado.

11 meses atrasado, mas acho que esta pergunta está pronta para algumas coisas do 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 usando-o apenas uma vez, então por que deveria 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 local.

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

A Wikipedia tem ótimos artigos comparando ambos Genéricos Java/C# e Genéricos Java/C++ modelos.O artigo principal sobre genéricos parece um pouco confuso, mas contém algumas informações boas.

A maior reclamação é o apagamento de tipo.Nesse sentido, os genéricos não são aplicados em tempo de execução. Aqui está um link para alguns documentos da Sun sobre o assunto.

Os genéricos são implementados por apagamento de tipo:As informações do tipo genérico estão presentes apenas no horário de compilação, após o que é apagado pelo compilador.

Os modelos C++ são, na verdade, muito mais poderosos do que seus equivalentes C# e Java, pois são avaliados em tempo de compilação e oferecem suporte à especialização.Isso permite a metaprogramação de modelos 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).

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

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

Observe 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.

Parece que, entre outras propostas muito interessantes, há uma sobre refinar genéricos e quebrar a retrocompatibilidade:

Atualmente, os genéricos são implementados usando o apagamento, o que significa que as informações do tipo genérico não estão disponíveis no tempo de execução, o que dificulta a gravação de algum tipo de código.Os genéricos foram implementados dessa maneira de oferecer suporte à compatibilidade com versões anteriores com o código não genérico mais antigo.Os genéricos reiificados disponibilizariam as informações do tipo genérico em tempo de execução, que quebrariam o código não genérico legado.No entanto, o Neal Gafter propôs tornar os tipos reificiáveis ​​apenas se especificados, para não quebrar a compatibilidade com versões anteriores.

no Artigo de Alex Miller sobre propostas do Java 7

Observação:Não tenho pontos suficientes para comentar, então sinta-se à vontade para mover isso como um comentário para a resposta apropriada.

Ao contrário da crença popular, da qual nunca entendi de onde veio, o .net implementou genéricos verdadeiros sem quebrar a compatibilidade com versões anteriores, e eles despenderam um esforço explícito para isso.Você não precisa alterar seu código .net 1.0 não genérico para genérico apenas para ser usado no .net 2.0.Ambas as listas genéricas e não genéricas ainda estão disponíveis no .Net framework 2.0 até a versão 4.0, exatamente por motivos de compatibilidade com versões anteriores.Portanto, códigos antigos que ainda usavam ArrayList não genérico ainda funcionarão e usarão a mesma classe ArrayList de antes.A compatibilidade com versões anteriores de código é sempre mantida desde 1.0 até agora...Portanto, mesmo no .net 4.0, você ainda tem a opção de usar qualquer classe não genérica do 1.0 BCL, se desejar.

Portanto, não acho que Java precise quebrar a compatibilidade com versões anteriores para oferecer suporte a genéricos verdadeiros.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top