203 votes

Quelles sont les différences entre les génériques en C# et Java... et les modèles en C++ ?

J'utilise principalement Java et les génériques sont relativement nouveaux. Je ne cesse 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 en matière de génériques ? Les avantages et les inconvénients de chacun ?

364voto

Orion Edwards Points 54939

Je vais ajouter ma voix au bruit et tenter de rendre les choses plus claires :

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

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

et alors le compilateur vous empêchera de mettre des choses qui ne sont pas Person dans la liste.
Dans les coulisses, le compilateur C# met simplement List<Person> dans le fichier .NET dll, 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 pour contenir les personnes - quelque chose comme ListOfPerson .

L'avantage de cette méthode est qu'elle est très rapide. Il n'y a pas de casting ou autre, et parce que la dll contient l'information qu'il s'agit d'une Liste de Person un autre code qui le regarde plus tard en utilisant la réflexion peut dire qu'il contient Person (afin de bénéficier de l'intellisense, etc.).

L'inconvénient est que l'ancien code C# 1.0 et 1.1 (avant l'ajout des génériques) ne comprend pas ces nouvelles fonctionnalités. List<something> Vous devez donc reconvertir manuellement les choses vers l'ancien format. List pour interopérer avec eux. Ce n'est pas un gros problème, car le code binaire de C# 2.0 n'est pas rétrocompatible. La seule fois où cela se produira est si vous mettez à niveau un vieux 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 apparence, c'est la même chose, et c'est un peu le cas. Le compilateur vous empêchera aussi de mettre des choses qui ne sont pas Person dans la liste.

La différence est ce qui se passe dans les coulisses. Contrairement à C#, Java ne construit pas un code spécial ListOfPerson - il utilise simplement le bon vieux ArrayList qui a toujours été en Java. Quand vous sortez des choses du tableau, les habituels Person p = (Person)foo.get(1); La danse du casting doit encore être faite. Le compilateur vous évite d'appuyer sur les touches, mais la vitesse d'exécution est toujours la même.
Quand les gens mentionnent "l'effacement de type", c'est de cela qu'ils parlent. Le compilateur insère les casts pour vous, puis "efface" le fait que ce soit censé être 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 de la même vieille ArrayList comme il l'a toujours fait. C'est plus important dans le monde Java parce qu'ils voulaient prendre en charge la compilation de code utilisant Java 5 avec les génériques, et le faire fonctionner sur les anciennes JVM 1.4 ou antérieures, ce que Microsoft a délibérément décidé de ne pas faire.

L'inconvénient est la perte de vitesse que j'ai mentionnée précédemment, et aussi parce qu'il n'y a pas d'accès à l'Internet. ListOfPerson pseudo-classe ou quoi que ce soit d'autre dans les fichiers .class, le code qui le regarde plus tard (avec la réflexion, ou si vous le tirez d'une autre collection où il a été converti en Object etc.) ne peuvent 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 cela fera ce que vous pensez qu'il devrait faire, mais en coulisses, différentes choses se passent.

Il a le plus de points communs avec les génériques de C#, en ce sens qu'il construit des génériques spéciaux. pseudo-classes plutôt que de rejeter les informations de type comme le fait Java, mais c'est une toute autre paire de manches.

C# et Java produisent tous deux des résultats conçus pour les machines virtuelles. Si vous écrivez du code qui a un Person dans celui-ci, dans les deux cas, des informations sur une classe de Person sera placé dans le fichier .dll ou .class, et la JVM/CLR en fera quelque chose.

Le C++ produit du code binaire brut x86. Tout est pas un objet, et il n'y a pas de machine virtuelle sous-jacente qui a besoin de connaître une Person classe. Il n'y a pas d'emballage ou de déballage, et les fonctions n'ont pas besoin d'appartenir à des classes, ou 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 - en fait, tout code que vous pourriez écrire manuellement, vous pouvez demander aux modèles de l'écrire pour vous.
L'exemple le plus évident est l'ajout d'éléments :

En C# et en Java, le système générique doit savoir quelles méthodes sont disponibles pour une classe, et il doit les transmettre à la machine virtuelle. La seule façon de le lui dire 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 compilera pas en C# ou en Java, car il ne sait pas que le type T fournit en fait une méthode appelée Name(). Vous devez lui dire - en C# comme ceci :

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

Ensuite, vous devez vous assurer que les éléments que vous passez à addNames implémentent l'interface IHasName et ainsi de suite. La syntaxe java est différente ( <T extends IHasName> ), mais il souffre des mêmes problèmes.

Le cas "classique" de ce problème est d'essayer d'écrire une fonction qui fait ceci

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

Vous ne pouvez pas réellement écrire ce code parce qu'il n'existe aucun moyen de déclarer une interface avec l'attribut + dans celui-ci. Vous échouez.

Le C++ ne souffre d'aucun de ces problèmes. Le compilateur ne se soucie pas de transmettre des types à une quelconque VM - si vos deux objets ont une fonction .Name(), il compilera. Si ce n'est pas le cas, il ne le fera pas. C'est simple.

Voilà, c'est fait :-)

61voto

Konrad Rudolph Points 231505

Le C++ utilise rarement la terminologie "générique". On utilise plutôt le mot "templates", qui est plus précis. Les modèles décrivent une technique pour obtenir une conception générique.

Les modèles C++ sont très différents de ce que C# et Java mettent en œuvre 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 aussi les arguments de valeur constante au moment de la compilation : les modèles peuvent être donnés sous forme d'entiers ou même de signatures de fonctions. Cela signifie qu'il est possible de faire des choses assez amusantes au moment de la compilation, par exemple des 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 particularité des templates C++, à savoir la spécialisation des templates. 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 Andrei Alexandrescu .

La spécialisation des modèles est importante pour le 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 les types. Cependant, bien que cela soit souhaitable, tous les types ne peuvent pas être traités de la même manière dans l'implémentation. Les templates C++ prennent cela en compte. C'est en grande partie la même différence que la POO fait entre l'interface et la mise en œuvre 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 un type de modèle et les traitent uniformément. En fait, ce n'est pas tout à fait exact : Le C++ ne travaille pas sur des conteneurs mais plutôt sur des gammes qui sont définis par deux itérateurs, pointant vers le début et vers la fin du conteneur. Ainsi, l'ensemble du contenu est circonscrit par les itérateurs : begin <= elements < end.

L'utilisation d'itérateurs au lieu de conteneurs est utile car elle permet d'opérer sur des parties d'un conteneur plutôt que sur l'ensemble.

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

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

Cela fonctionne pour tout type d'élément. Mais disons que nous pouvons stocker les pointeurs de manière plus efficace que les autres types en appliquant une astuce spéciale. Nous pouvons le faire en partiellement se spécialisant pour tous les types de pointeurs :

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

Désormais, chaque fois que l'on instaure 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*.

35voto

jfs Points 13605

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

18voto

Jörg W Mittag Points 153275

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

Comme nous l'avons déjà expliqué, la principale différence est effacement de type Il s'agit du 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 la suivante : pourquoi quelqu'un ferait-il cela ? Cela n'a aucun sens ! Ou alors ?

Eh bien, quelle est l'alternative ? Si vous n'implémentez pas les génériques dans le langage, où se trouve 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. En d'autres termes : le code qui a été compilé sur Java 5 peut toujours être déployé sur Java 1.4.

Microsoft a toutefois 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, les Sun ne sont pas des idiots ou des lâches. La raison pour laquelle ils se sont "dégonflés", c'est 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 en même temps dans les deux mondes.) Rompre la rétrocompatibilité aurait été une énorme douleur.

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

Comparez cela aux fonctionnalités de .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 toutes des fonctionnalités de l'industrie de l'informatique. 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 en tous les langues. Mais le CLR n'a pas changé : il est toujours le même dans .NET 3.5 SP1 qu'il l'était dans .NET 2.0. Vous pouvez compiler un programme C# qui utilise LINQ avec le compilateur .NET 3.5 et l'exécuter sur .NET 2.0, à condition de ne pas utiliser de bibliothèques .NET 3.5. Cela signifie que pas fonctionne avec les génériques et .NET 1.1, mais il serait fonctionnent avec Java et Java 1.4.

14voto

Konrad Rudolph Points 231505

Suivi de mon message précédent.

Les modèles sont l'une des principales raisons pour lesquelles le C++ échoue de manière si abyssale à l'intellisense, quel que soit l'IDE utilisé. En raison de la spécialisation des modèles, l'IDE ne peut jamais être vraiment sûr qu'un membre donné existe ou non. Pensez-y :

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 très difficile pour l'IDE de dire à ce moment-là si, et quoi, les membres a a. Pour d'autres langages, l'analyse syntaxique serait simple, mais pour le C++, un certain nombre d'évaluations préalables sont nécessaires.

C'est encore pire. Et si my_int_type ont été définis à l'intérieur d'un modèle de classe également ? 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 le précédent : Y<int>::my_type se résout à int donc b doit être du même type que a n'est-ce pas ?

Faux. Au moment où le compilateur essaye de résoudre cette déclaration, 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 exemple 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 nous référons à un nom de type :

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

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

Y<int>::my_type(123);

Cette instruction de code est parfaitement valide et indique au 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 déclaration serait toujours valide et effectuerait un cast spécial (le function-style cast) qui est souvent une invocation de constructeur. Le compilateur ne peut pas dire ce que nous voulons dire, donc nous devons désambiguïser ici.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X