Quelles sont les différences entre les génériques en C# et Java… et les modèles en C++ ?[fermé]

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

Question

J'utilise principalement Java et les génériques sont relativement nouveaux.Je continue de lire que Java a pris la mauvaise décision ou que .NET a de meilleures implémentations, etc.etc.

Alors, quelles sont les principales différences entre C++, C#, Java dans les génériques ?Avantages/inconvénients de chacun ?

Était-ce utile?

La solution

Je vais ajouter ma voix au bruit et essayer de clarifier les choses :

Les génériques C# vous permettent de déclarer quelque chose comme ceci.

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

et ensuite le compilateur vous empêchera de mettre des choses qui ne le sont pas Person dans la liste.
En coulisses, le compilateur C# ne fait que mettre List<Person> dans le fichier dll .NET, mais au moment de l'exécution, le compilateur JIT construit un nouvel ensemble de code, comme si vous aviez écrit une classe de liste spéciale uniquement pour contenir des personnes - quelque chose comme ListOfPerson.

L’avantage est que cela rend les choses très rapides.Il n'y a pas de casting ou autre, et comme la DLL contient l'information selon laquelle il s'agit d'une liste de Person, un autre code qui l'examine plus tard en utilisant la réflexion peut dire qu'il contient Person objets (donc vous obtenez l'intellisense et ainsi de suite).

L'inconvénient est que l'ancien code C# 1.0 et 1.1 (avant d'ajouter des génériques) ne comprend pas ces nouveaux List<something>, vous devez donc reconvertir manuellement les choses à l'ancienne List pour interagir avec eux.Ce n’est pas un gros problème, car le code binaire C# 2.0 n’est pas rétrocompatible.La seule fois où cela se produira, c'est si vous mettez à niveau un ancien code C# 1.0/1.1 vers C# 2.0.

Les génériques Java vous permettent de déclarer quelque chose comme ceci.

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

En surface, cela semble pareil, et c’est en quelque sorte le cas.Le compilateur vous empêchera également de mettre des éléments qui ne le sont pas. Person dans la liste.

La différence réside dans ce qui se passe dans les coulisses.Contrairement à C#, Java ne construit pas de programme spécial ListOfPerson - il utilise juste le bon vieux ArrayList qui a toujours été en Java.Lorsque vous retirez des éléments du tableau, l'habituel Person p = (Person)foo.get(1); Le casting-dance reste à faire.Le compilateur vous épargne les pressions sur les touches, mais la vitesse d'accès/diffusion est toujours appliquée comme elle l'a toujours été.
Lorsque les gens mentionnent « Type Erasure », c'est de cela qu'ils parlent.Le compilateur insère les transtypages pour vous, puis « efface » le fait qu'il s'agit d'une liste de Person pas seulement Object

L'avantage de cette approche est que l'ancien code qui ne comprend pas les génériques n'a pas à s'en soucier.Il s'agit toujours du même vieux ArrayList comme cela a toujours été le cas.Ceci est plus important dans le monde Java car ils voulaient prendre en charge la compilation de code en utilisant Java 5 avec des génériques et le faire fonctionner sur les anciennes JVM 1.4 ou précédentes, avec lesquelles Microsoft a délibérément décidé de ne pas s'embêter.

L'inconvénient est la vitesse atteinte que j'ai mentionnée précédemment, et aussi parce qu'il n'y a pas de ListOfPerson pseudo-classe ou quelque chose comme ça qui entre dans les fichiers .class, du code qui l'examine plus tard (avec réflexion, ou si vous le retirez d'une autre collection où il a été converti en Object ou ainsi de suite) ne peut en aucun cas dire qu'il s'agit d'une liste contenant uniquement Person et pas n'importe quelle autre liste de tableaux.

Les modèles C++ vous permettent de déclarer quelque chose comme ceci

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

Cela ressemble aux génériques C# et Java, et il fera ce que vous pensez qu'il devrait faire, mais dans les coulisses, différentes choses se produisent.

Il a le plus de points communs avec les génériques C# dans la mesure où il construit des pseudo-classes plutôt que de simplement jeter les informations de type comme le fait Java, mais c'est une toute autre marmite de poisson.

C# et Java produisent tous deux une sortie conçue pour les machines virtuelles.Si vous écrivez du code qui a un Person classe, dans les deux cas, des informations sur un Person La classe ira dans le fichier .dll ou .class, et la JVM/CLR fera des choses avec cela.

C++ produit du code binaire x86 brut.Tout est pas un objet, et il n'y a pas de machine virtuelle sous-jacente qui a besoin de connaître un Person classe.Il n'y a pas de boxing ni de unboxing, et les fonctions ne doivent pas nécessairement appartenir à des classes, ni même à quoi que ce soit.

Pour cette raison, le compilateur C++ n'impose aucune restriction sur ce que vous pouvez faire avec les modèles - essentiellement, n'importe quel code que vous pouvez écrire manuellement, vous pouvez demander aux modèles d'écrire pour vous.
L’exemple le plus évident est l’ajout d’éléments :

En C# et Java, le système générique doit savoir quelles méthodes sont disponibles pour une classe et doit les transmettre à la machine virtuelle.La seule façon de le savoir est soit de coder en dur la classe réelle, soit d'utiliser des interfaces.Par exemple:

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

Ce code ne sera pas compilé en C# ou Java, car il ne sait pas que le type T fournit en fait une méthode appelée Name().Vous devez le dire - en C# comme ceci :

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

Et puis vous devez vous assurer que les éléments que vous transmettez à addNames implémentent l'interface IHasName, etc.La syntaxe Java est différente (<T extends IHasName>), mais il souffre des mêmes problèmes.

Le cas « classique » de ce problème consiste à essayer d’écrire une fonction qui fait cela

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

Vous ne pouvez pas réellement écrire ce code car il n'existe aucun moyen de déclarer une interface avec le + méthode dedans.Vous échouez.

C++ ne souffre d’aucun de ces problèmes.Le compilateur ne se soucie pas de transmettre les types à n'importe quelle machine virtuelle - si vos deux objets ont une fonction .Name(), il sera compilé.S’ils ne le font pas, ce ne sera pas le cas.Simple.

Alors voilà :-)

Autres conseils

C++ utilise rarement la terminologie « génériques ».Au lieu de cela, le mot « modèles » est utilisé et est plus précis.Les modèles en décrivent un technique pour parvenir à une conception générique.

Les modèles C++ sont très différents de ce que C# et Java implémentent pour deux raisons principales.La première raison est que les modèles C++ n'autorisent pas seulement les arguments de type au moment de la compilation, mais également les arguments de valeur const au moment de la compilation :les modèles peuvent être donnés sous forme d'entiers ou même de signatures de fonction.Cela signifie que vous pouvez faire des choses assez géniales au moment de la compilation, par ex.calculs :

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;

Ce code utilise également l'autre fonctionnalité distinctive des modèles C++, à savoir la spécialisation des modèles.Le code définit un modèle de classe, product qui a un argument de valeur.Il définit également une spécialisation pour ce modèle qui est utilisée chaque fois que l'argument est évalué à 1.Cela me permet de définir une récursion sur les définitions de modèles.Je crois que cela a été découvert pour la première fois par Andreï Alexandrescu.

La spécialisation des modèles est importante pour C++ car elle permet des différences structurelles dans les structures de données.Les modèles dans leur ensemble sont un moyen d’unifier une interface entre types.Cependant, bien que cela soit souhaitable, tous les types ne peuvent pas être traités de la même manière au sein de l’implémentation.Les modèles C++ en tiennent compte.C'est à peu près la même différence que la POO fait entre l'interface et l'implémentation avec le remplacement des méthodes virtuelles.

Les modèles C++ sont essentiels pour son paradigme de programmation algorithmique.Par exemple, presque tous les algorithmes pour les conteneurs sont définis comme des fonctions qui acceptent le type de conteneur comme type de modèle et les traitent de manière uniforme.En fait, ce n'est pas tout à fait vrai :C++ ne fonctionne pas sur les conteneurs mais plutôt sur gammes qui sont définis par deux itérateurs, pointant vers le début et derrière la fin du conteneur.Ainsi, tout le contenu est circonscrit par les itérateurs :début <= éléments < fin.

Utiliser des itérateurs au lieu de conteneurs est utile car cela permet d’opérer sur des parties d’un conteneur plutôt que sur l’ensemble.

Une autre particularité du C++ est la possibilité de spécialisation partielle pour les modèles de classe.Ceci est quelque peu lié à la correspondance de modèles sur les arguments dans Haskell et d'autres langages fonctionnels.Par exemple, considérons une classe qui stocke des éléments :

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

Cela fonctionne pour n’importe quel type d’élément.Mais disons que nous pouvons stocker les pointeurs plus efficacement que les autres types en appliquant une astuce spéciale.Nous pouvons le faire en partiellement spécialisé pour tous les types de pointeurs :

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

Désormais, chaque fois que nous instancions un modèle de conteneur pour un type, la définition appropriée est utilisée :

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

Anders Hejlsberg lui-même a décrit ici les différences "Génériques en C#, Java et C++".

Il y a déjà beaucoup de bonnes réponses sur quoi les différences sont là, alors permettez-moi de donner une perspective légèrement différente et d'ajouter le pourquoi.

Comme cela a déjà été expliqué, la principale différence réside dans tapez l'effacement, c'est à dire.le fait que le compilateur Java efface les types génériques et qu'ils ne se retrouvent pas dans le bytecode généré.Cependant, la question est :pourquoi quelqu'un ferait-il ça ?Cela n'a pas de sens !Ou est-ce que c'est le cas ?

Eh bien, quelle est l'alternative ?Si vous n'implémentez pas de génériques dans le langage, où faire vous les mettez en œuvre ?Et la réponse est :dans la machine virtuelle.Ce qui rompt la rétrocompatibilité.

L'effacement de type, en revanche, vous permet de mélanger des clients génériques avec des bibliothèques non génériques.Autrement dit:le code compilé sur Java 5 peut toujours être déployé sur Java 1.4.

Microsoft a cependant décidé de rompre la rétrocompatibilité pour les génériques. C'est pourquoi les génériques .NET sont « meilleurs » que les génériques Java.

Bien sûr, Sun n’est ni idiot ni lâche.La raison pour laquelle ils se sont « dégonflés » était que Java était nettement plus ancien et plus répandu que .NET lorsqu’ils ont introduit les génériques.(Ils ont été introduits à peu près au même moment dans les deux mondes.) Rompre la rétrocompatibilité aurait été très pénible.

En d’autres termes :en Java, les génériques font partie du Langue (ce qui signifie qu'ils appliquent seulement vers Java, pas vers d'autres langages), dans .NET, ils font partie du Machine virtuelle (ce qui signifie qu'ils s'appliquent à tous langages, pas seulement C# et Visual Basic.NET).

Comparez cela avec les fonctionnalités .NET telles que LINQ, les expressions lambda, l'inférence de type de variable locale, les types anonymes et les arbres d'expression :ce sont tous langue caractéristiques.C'est pourquoi il existe des différences subtiles entre VB.NET et C# :si ces fonctionnalités faisaient partie de la VM, elles seraient les mêmes dans tous langues.Mais le CLR n'a pas changé :c'est toujours la même chose dans .NET 3.5 SP1 que dans .NET 2.0.Vous pouvez compiler un programme C# qui utilise LINQ avec le compilateur .NET 3.5 et toujours l'exécuter sur .NET 2.0, à condition de n'utiliser aucune bibliothèque .NET 3.5.Qui serait pas fonctionne avec les génériques et .NET 1.1, mais cela serait travailler avec Java et Java 1.4.

Suite à mon message précédent.

Les modèles sont l’une des principales raisons pour lesquelles C++ échoue si catégoriquement avec Intellisense, quel que soit l’EDI utilisé.En raison de la spécialisation des modèles, l'EDI ne peut jamais être vraiment sûr si un membre donné existe ou non.Considérer:

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

template <>
struct X<int> { };

typedef int my_int_type;

X<my_int_type> a;
a.|

Maintenant, le curseur est à la position indiquée et il est sacrément difficile pour l'IDE de dire à ce stade si, et quoi, les membres a a.Pour d'autres langages, l'analyse serait simple, mais pour C++, un peu d'évaluation est nécessaire au préalable.

Ça s'empire.Et si my_int_type ont également été définis dans un modèle de classe ?Maintenant, son type dépendrait d'un autre argument de type.Et ici, même les compilateurs échouent.

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

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

Après un peu de réflexion, un programmeur conclurait que ce code est le même que celui ci-dessus : Y<int>::my_type décide de int, donc b devrait être du même type que a, droite?

Faux.Au moment où le compilateur tente de résoudre cette instruction, il ne sait pas réellement Y<int>::my_type encore!Par conséquent, il ne sait pas qu’il s’agit d’un type.Cela pourrait être autre chose, par ex.une fonction membre ou un champ.Cela pourrait donner lieu à des ambiguïtés (mais pas dans le cas présent), donc le compilateur échoue.Nous devons lui dire explicitement que nous faisons référence à un nom de type :

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

Maintenant, le code se compile.Pour voir comment des ambiguïtés naissent de cette situation, considérons le code suivant :

Y<int>::my_type(123);

Cette instruction de code est parfaitement valide et indique à C++ d'exécuter l'appel de fonction à Y<int>::my_type.Toutefois, si my_type n'est pas une fonction mais plutôt un type, cette instruction serait toujours valide et effectuerait un transtypage spécial (le transtypage de style fonction) qui est souvent un invocation de constructeur.Le compilateur ne peut pas dire ce que nous voulons dire, nous devons donc lever l'ambiguïté ici.

Java et C# ont tous deux introduit des génériques après la première version de leur langage.Cependant, il existe des différences dans la façon dont les bibliothèques principales ont changé lorsque les génériques ont été introduits. Les génériques de C# ne sont pas seulement la magie du compilateur et donc il n'était pas possible de générer classes de bibliothèque existantes sans rompre la compatibilité ascendante.

Par exemple, en Java, l'existant Cadre de collecte était complètement générique. Java ne dispose pas de version générique et non générique des classes de collections. À certains égards, c'est beaucoup plus propre - si vous devez utiliser une collection en C#, il y a vraiment très peu de raisons d'opter pour la version non générique, mais ces classes héritées restent en place, encombrant le paysage.

Une autre différence notable concerne les classes Enum en Java et C#. Enum de Java a cette définition quelque peu tortueuse :

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

(voir le discours très clair d'Angelika Langer explication de pourquoi exactement c'est tellement.Essentiellement, cela signifie que Java peut donner un accès sécurisé de type depuis une chaîne à sa valeur Enum :

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

Comparez ceci à la version de C# :

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

Comme Enum existait déjà en C# avant l'introduction des génériques dans le langage, la définition ne pouvait pas changer sans casser le code existant.Ainsi, comme les collections, il reste dans les bibliothèques principales dans cet état hérité.

11 mois de retard, mais je pense que cette question est prête pour des trucs Java Wildcard.

Il s'agit d'une fonctionnalité syntaxique de Java.Supposons que vous ayez une méthode :

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

Et supposons que vous n'ayez pas besoin de faire référence au type T dans le corps de la méthode.Vous déclarez un nom T et ne l'utilisez ensuite qu'une seule fois, alors pourquoi devriez-vous penser à lui trouver un nom ?A la place, vous pouvez écrire :

public void Foo(Collection<?> thing)

Le point d'interrogation demande au compilateur de prétendre que vous avez déclaré un paramètre de type nommé normal qui ne doit apparaître qu'une seule fois à cet endroit.

Il n'y a rien que vous puissiez faire avec les caractères génériques que vous ne puissiez faire avec un paramètre de type nommé (c'est ainsi que ces choses se font toujours en C++ et C#).

Wikipédia propose d'excellents articles comparant les deux Génériques Java/C# et Génériques Java/C++ modèles.Le article principal sur les génériques semble un peu encombré mais il contient de bonnes informations.

Le plus gros reproche concerne l’effacement des caractères.En cela, les génériques ne sont pas appliqués au moment de l’exécution. Voici un lien vers quelques documents Sun sur le sujet.

Les génériques sont implémentés par l'effacement de type:Les informations de type générique sont présentes uniquement au moment de la compilation, après quoi elles sont effacées par le compilateur.

Les modèles C++ sont en réalité beaucoup plus puissants que leurs homologues C# et Java car ils sont évalués au moment de la compilation et prennent en charge la spécialisation.Cela permet la méta-programmation de modèles et rend le compilateur C++ équivalent à une machine de Turing (c'est-à-dirependant le processus de compilation, vous pouvez calculer tout ce qui est calculable avec une machine de Turing).

En Java, les génériques sont uniquement au niveau du compilateur, vous obtenez donc :

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

Notez que le type de « a » est une liste de tableaux, pas une liste de chaînes.Ainsi, le type d’une liste de bananes serait égal() à une liste de singes.

Pour ainsi dire.

Il semble que, parmi d'autres propositions très intéressantes, il y en ait une sur le raffinement des génériques et la rupture de la compatibilité ascendante :

Actuellement, les génériques sont implémentés à l'aide de l'effacement, ce qui signifie que les informations de type générique ne sont pas disponibles lors de l'exécution, ce qui rend une sorte de code difficile à écrire.Les génériques ont été implémentés de cette façon pour prendre en charge la compatibilité en arrière avec le code non générique plus ancien.Les génériques réifiés rendraient les informations de type générique disponibles lors de l'exécution, ce qui casserait le code non générique hérité.Cependant, Neal Gafter a proposé de rendre les types réifiables uniquement s'ils sont spécifiés, afin de ne pas rompre la compatibilité en arrière.

à Article d'Alex Miller sur les propositions Java 7

Attention :Je n'ai pas assez de points à commenter, alors n'hésitez pas à déplacer ceci en tant que commentaire vers une réponse appropriée.

Contrairement à la croyance populaire, dont je ne comprends jamais d'où cela vient, .net a implémenté de véritables génériques sans rompre la compatibilité ascendante, et ils ont déployé des efforts explicites pour cela.Vous n'avez pas besoin de changer votre code .net 1.0 non générique en génériques uniquement pour être utilisé dans .net 2.0.Les listes génériques et non génériques sont toujours disponibles dans .Net Framework 2.0 même jusqu'à la version 4.0, exactement pour des raisons de compatibilité ascendante.Par conséquent, les anciens codes qui utilisaient encore ArrayList non générique fonctionneront toujours et utiliseront la même classe ArrayList qu'auparavant.La compatibilité ascendante du code est toujours maintenue depuis la version 1.0 jusqu'à maintenant...Ainsi, même dans .net 4.0, vous devez toujours choisir d'utiliser n'importe quelle classe non générique de la version 1.0 BCL si vous choisissez de le faire.

Je ne pense donc pas que Java doive rompre la compatibilité ascendante pour prendre en charge les vrais génériques.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top