Respostas:
Vou acrescentar minha voz ao barulho e tentar esclarecer as coisas:
List<Person> foo = new List<Person>();
e o compilador impedirá que você coloque coisas que não estão Person
na lista.
Nos bastidores, o compilador C # está apenas List<Person>
inserindo o arquivo dll .NET, mas em tempo de execução o compilador JIT cria e constrói um novo conjunto de códigos, como se você tivesse escrito uma classe de lista especial apenas para conter pessoas - algo como ListOfPerson
.
O benefício disso é que o torna muito rápido. Não há conversão ou qualquer outra coisa, e como a dll contém as informações de que é uma lista Person
, outro código que a analisa mais tarde usando a reflexão pode dizer que ela contém Person
objetos (para que você entenda bem o sentido).
A desvantagem disso é que o código C # 1.0 e 1.1 antigo (antes de adicionarem genéricos) não entende esses novos List<something>
, portanto, você deve converter manualmente as coisas novamente para antigas simples List
para interoperar com elas. Esse não é um grande problema, porque o código binário do 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
ArrayList<Person> foo = new ArrayList<Person>();
Na superfície, parece o mesmo, e meio que é. O compilador também impedirá que você coloque coisas que não sãoPerson
na lista.
A diferença é o que acontece nos bastidores. Ao contrário do C #, o Java não cria um especial ListOfPerson
- apenas usa o antigo simples ArrayList
que sempre esteve em Java. Quando você tira as coisas da matriz, a Person p = (Person)foo.get(1);
dança do elenco habitual ainda precisa ser feita. O compilador está poupando as teclas pressionadas, mas a velocidade de execução / conversão ainda é incorrida como sempre.
Quando as pessoas mencionam "Eliminação de tipo", é sobre isso que estão falando. O compilador insere os modelos para você e depois 'apaga' o fato de que ele deve ser uma lista Person
não apenasObject
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 velhoArrayList
sempre. Isso é mais importante no mundo java porque eles queriam oferecer suporte à compilação de código usando o Java 5 com genéricos e a execução em JVMs 1.4 ou anteriores, que a Microsoft decidiu deliberadamente não se incomodar.
A desvantagem é a velocidade atingida que mencionei anteriormente, e também porque não há ListOfPerson
pseudo-classe ou algo parecido nos arquivos .class, código que o analisa mais tarde (com reflexão ou se você retirá-lo de outra coleção). onde foi convertido Object
ou assim por diante) não pode dizer de forma alguma que deve ser uma lista contendo apenas Person
e não apenas qualquer outra lista de matrizes.
std::list<Person>* foo = new std::list<Person>();
Parece C # e Java genéricos e fará o que você acha que deve fazer, mas nos bastidores coisas diferentes estão acontecendo.
Ele tem o mais em comum com os genéricos de C #, na medida em que cria informações especiais, em pseudo-classes
vez de apenas jogar fora as informações de tipo, como o java, mas é uma chaleira de peixe totalmente diferente.
C # e Java produzem saída projetada para máquinas virtuais. Se você escrever algum código que contenha uma Person
classe, em ambos os casos algumas informações sobre uma Person
classe serão inseridas no arquivo .dll ou .class, e a JVM / CLR fará as coisas necessárias.
C ++ produz código binário x86 bruto. Tudo não é um objeto e não há uma máquina virtual subjacente que precise saber sobre umPerson
classe. Não há boxe ou unboxing, e as funções não precisam pertencer a classes, ou mesmo qualquer coisa.
Por causa disso, o compilador C ++ não impõe restrições ao 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 deve passar isso para a máquina virtual. A única maneira de dizer 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 ele não sabe que o tipo T
realmente fornece um método chamado Name (). Você precisa dizer - em C # assim:
interface IHasName{ string Name(); };
string addNames<T>( T first, T second ) where T : IHasName { .... }
E você precisa se certificar de que as coisas que você passa para addNames implementam a interface IHasName e assim por diante. A sintaxe do java é diferente ( <T extends IHasName>
), mas sofre dos mesmos problemas.
O caso "clássico" para esse problema está tentando escrever uma função que faz isso
string addNames<T>( T first, T second ) { return first + second; }
Você não pode realmente escrever esse código porque não há maneiras de declarar uma interface com o +
método nele. Você falhou.
C ++ sofre de nenhum desses problemas. O compilador não se importa em passar tipos para nenhuma VM - se os dois objetos tiverem uma função .Name (), ele será compilado. Se não, não vai. Simples.
Então, aí está :-)
int addNames<T>( T first, T second ) { return first + second; }
em c #. O tipo genérico pode ser restrito a uma classe em vez de uma interface, e existe uma maneira de declarar uma classe com o +
operador nela.
O C ++ raramente usa a terminologia "genérica". Em vez disso, a palavra "modelos" é usada e é mais precisa. Modelos descreve uma 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 apenas permitem argumentos do tipo tempo de compilação, mas também argumentos de valor const em tempo de compilação: os modelos podem ser dados como números inteiros ou mesmo como assinaturas de funções. Isso significa que você pode fazer algumas coisas bastante divertidas 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 ++, a especialização de modelos. 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 me permite definir uma recursão sobre as definições de modelo. Eu acredito que isso foi descoberto por Andrei Alexandrescu .
A especialização de modelos é importante para C ++, pois permite diferenças estruturais nas estruturas de dados. Modelos como um todo é 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 consideração. Essa é a mesma diferença que o OOP faz entre interface e implementação com a substituição de 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 de maneira uniforme. Na verdade, isso não está certo: o C ++ não funciona em contêineres, mas em intervalos definidos por dois iteradores, apontando para o início e 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.
Outro recurso distintivo do C ++ é a possibilidade de especialização parcial para modelos de classe. Isso está relacionado à correspondência de padrões nos argumentos do 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 ponteiros com mais eficiência do que outros tipos, aplicando algum truque especial. Podemos fazer isso especializando-nos parcialmente para todos os tipos de ponteiros:
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 quais são as diferenças, então deixe-me dar uma perspectiva um pouco diferente e acrescentar o porquê .
Como já foi explicado, a principal diferença é o apagamento de tipo , ou seja, o fato de o compilador Java apagar os tipos genéricos e eles não acabam no bytecode gerado. No entanto, a pergunta é: por que alguém faria isso? Não faz sentido! Ou faz?
Bem, qual é a alternativa? Se você não implementar genéricos na língua, onde fazer implementa? E a resposta é: na máquina virtual. O que quebra a compatibilidade com versões anteriores.
Por outro lado, o apagamento de tipo permite combinar 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 com os genéricos. É por isso que o .NET Generics é "melhor" que o Java Generics.
É claro que a Sun não é idiota ou covarde. A razão pela qual eles se destacaram foi que o Java era significativamente mais antigo e mais difundido que o .NET quando eles introduziram genéricos. (Eles foram introduzidos aproximadamente ao mesmo tempo nos dois mundos.) Quebrar a compatibilidade com versões anteriores teria sido uma grande dor.
Dito de outra maneira: em Java, os genéricos fazem parte da linguagem (o que significa que eles se aplicam apenas a 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 os recursos do .NET, como LINQ, expressões lambda, inferência de tipo de variável local, tipos anônimos e árvores de expressão: esses são todos os recursos de idioma . É por isso que existem diferenças sutis entre VB.NET e C #: se esses recursos fizessem parte da VM, seriam iguais em todos os idiomas. Mas o CLR não mudou: ainda é o mesmo no .NET 3.5 SP1 como 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 você não use nenhuma biblioteca do .NET 3.5. Isso não trabalho com os genéricos e .NET 1.1, mas iria trabalhar com Java e Java 1.4.
ArrayList<T>
pode ser emitido como um novo tipo nomeado internamente com um Class<T>
campo estático (oculto) . Desde que a nova versão da lib genérica tenha sido implementada com o código de 1,5 ou mais bytes, ela poderá ser executada em 1.4-JVMs.
Acompanhamento da minha postagem anterior.
Os modelos são um dos principais motivos pelos quais o C ++ falha de maneira tão absurda no intellisense, independentemente do IDE usado. Devido à especialização de modelos, o IDE nunca pode ter certeza se um 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 momento se, e o que, membros a
têm. Para outros idiomas, a análise seria simples, mas para C ++, é necessária uma certa avaliação prévia.
Fica pior. E se também my_int_type
fossem definidos 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 quea
, certo?
Errado. No momento em que o compilador tenta resolver esta declaração, ele ainda não sabe Y<int>::my_type
! Portanto, ele não sabe que esse é um tipo. Pode ser outra coisa, por exemplo, uma função 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 ambiguidades surgem dessa situação, considere o seguinte código:
Y<int>::my_type(123);
Essa declaração de código é perfeitamente válida e diz ao C ++ para executar a chamada de função Y<int>::my_type
. No entanto, se my_type
não for uma função, mas um tipo, essa instrução ainda seria válida e executaria uma conversão especial (a conversão no estilo de função), que geralmente é uma invocação de construtor. O compilador não pode dizer o que queremos dizer, então temos que desambiguar aqui.
Java e C # introduziram genéricos após o lançamento do primeiro idioma. No entanto, existem diferenças em como as bibliotecas principais foram alteradas quando os genéricos foram introduzidos. Os genéricos do C # não são apenas mágicos do compilador e, portanto, não foi possível gerar classes de bibliotecas existentes sem quebrar a compatibilidade com versões anteriores.
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 herdada das classes de coleções. De certa forma, isso é muito mais limpo - se você precisar usar uma coleção em C #, há muito pouco motivo para usar a versão não genérica, mas essas classes herdadas permanecem no lugar, ocupando a paisagem.
Outra diferença notável são 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 é assim. Essencialmente, isso significa que o Java pode conceder acesso seguro ao tipo de uma string para o 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 da introdução de genéricos no idioma, a definição não pôde ser alterada sem a quebra do código existente. Assim, como coleções, ele permanece nas bibliotecas principais nesse estado herdado.
ArrayList
para List<T>
e colocá-lo em um novo namespace. O fato é que, se uma classe aparecer no código-fonte, ArrayList<T>
ela se tornará um nome de classe diferente gerado no compilador no código IL, portanto, não haverá conflitos de nome.
11 meses atrasado, mas acho que esta pergunta está pronta para algumas coisas sobre 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 depois o usa apenas uma vez; então, por que você deveria pensar em um nome para ele? Em vez disso, você pode escrever:
public void Foo(Collection<?> thing)
O ponto de interrogação solicita ao compilador que finja que você declarou um parâmetro de tipo nomeado normal que só precisa aparecer uma vez nesse local.
Não há nada que você possa fazer com caracteres curinga que você também não possa fazer com um parâmetro de tipo nomeado (que é como essas coisas sempre são feitas em C ++ e C #).
class Foo<T extends List<?>>
e usar, Foo<StringList>
mas em C # você precisa adicionar esse parâmetro de tipo extra: class Foo<T, T2> where T : IList<T2>
e usar o desajeitado Foo<StringList, String>
.
A Wikipedia possui ótimas redações comparando os modelos Java / C # genéricos e Java genéricos / C ++ . O artigo principal sobre genéricos parece um pouco confuso, mas contém algumas informações boas.
A maior reclamação é o apagamento do tipo. Nesse sentido, os genéricos não são impostos em tempo de execução. Aqui está um link para alguns documentos da Sun sobre o assunto .
Os genéricos são implementados por tipo de apagamento: as informações de tipo genérico estão presentes apenas no tempo de compilação, após o que são apagadas pelo compilador.
Na verdade, os modelos C ++ são 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 meta-programaçã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 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 matrizes, não uma lista de seqüências de caracteres. Portanto, o tipo de uma lista de bananas seria igual a () uma lista de macacos.
Por assim dizer.
Parece que, entre outras propostas muito interessantes, existe uma sobre refinar genéricos e quebrar a compatibilidade com versões anteriores:
Atualmente, os genéricos são implementados usando apagamento, o que significa que as informações genéricas do tipo 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 para oferecer suporte à compatibilidade com versões anteriores de códigos não genéricos mais antigos. Os genéricos reificados disponibilizariam as informações de tipo genérico em tempo de execução, o que quebraria o código não genérico herdado. No entanto, Neal Gafter propôs tornar os tipos reificáveis somente se especificado, para não prejudicar a compatibilidade com versões anteriores.
NB: Não tenho argumentos suficientes para comentar, portanto, sinta-se à vontade para mover isso como um comentário para a resposta apropriada.
Ao contrário da crença popular, que eu nunca entendo de onde veio, o .net implementou verdadeiros genéricos sem quebrar a compatibilidade com versões anteriores, e eles gastaram esforços explícitos para isso. Você não precisa alterar seu código .net 1.0 não genérico em genéricos apenas para ser usado no .net 2.0. As listas genéricas e não genéricas ainda estão disponíveis no .Net framework 2.0 até 4.0, exatamente por outro motivo, exceto por compatibilidade com versões anteriores. Portanto, os 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 o código anterior é sempre mantida desde a versão 1.0 até agora ... Portanto, mesmo no .net 4.0, você ainda precisa usar qualquer classe não genérica da 1.0 BCL, se optar por fazê-lo.
Portanto, não acho que o java precise quebrar a compatibilidade com versões anteriores para oferecer suporte a verdadeiros genéricos.
ArrayList<Foo>
que ele queira passar para um método mais antigo que deve preencher um ArrayList
com instâncias de Foo
. Se um ArrayList<foo>
não é um ArrayList
, como alguém faz isso funcionar?