Was sind die Unterschiede zwischen Generics in C# und Java … und Templates in C++?[geschlossen]

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

Frage

Ich verwende hauptsächlich Java und Generika sind relativ neu.Ich lese immer wieder, dass Java die falsche Entscheidung getroffen hat oder dass .NET bessere Implementierungen hat usw.usw.

Was sind also die Hauptunterschiede zwischen C++, C# und Java in Generika?Vor- und Nachteile von jedem?

War es hilfreich?

Lösung

Ich füge meine Stimme zum Lärm hinzu und versuche, die Dinge klar zu machen:

Mit C#-Generika können Sie so etwas deklarieren.

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

und dann wird der Compiler Sie daran hindern, Dinge einzufügen, die nicht vorhanden sind Person in die Liste eintragen.
Hinter den Kulissen steckt gerade der C#-Compiler List<Person> in die .NET-DLL-Datei, aber zur Laufzeit erstellt der JIT-Compiler einen neuen Satz Code, als hätten Sie eine spezielle Listenklasse nur für die Aufnahme von Personen geschrieben – so etwas wie ListOfPerson.

Der Vorteil davon ist, dass es wirklich schnell geht.Es gibt kein Casting oder andere Dinge, und da die DLL die Informationen enthält, handelt es sich hierbei um eine Liste von Person, kann anderer Code, der es später mithilfe von Reflektion betrachtet, erkennen, dass es Folgendes enthält Person Objekte (damit man Intellisense erhält und so weiter).

Der Nachteil dabei ist, dass der alte C# 1.0- und 1.1-Code (bevor sie Generika hinzufügten) diese neuen nicht versteht List<something>, also müssen Sie die Dinge manuell wieder in die alte Version umwandeln List mit ihnen zu interagieren.Dies stellt kein großes Problem dar, da der Binärcode von C# 2.0 nicht abwärtskompatibel ist.Dies kann nur passieren, wenn Sie alten C# 1.0/1.1-Code auf C# 2.0 aktualisieren

Mit Java Generics können Sie so etwas deklarieren.

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

Oberflächlich betrachtet sieht es gleich aus und ist es auch.Der Compiler verhindert auch, dass Sie Dinge einfügen, die nicht vorhanden sind Person in die Liste eintragen.

Der Unterschied besteht darin, was hinter den Kulissen passiert.Im Gegensatz zu C# erstellt Java kein spezielles ListOfPerson - Es wird nur das einfache Alte verwendet ArrayList was schon immer in Java war.Wenn Sie Dinge aus dem Array herausholen, ist das Übliche Person p = (Person)foo.get(1); Casting-Dance muss noch gemacht werden.Der Compiler erspart Ihnen das Drücken von Tasten, aber die Geschwindigkeitsbeeinträchtigung/-übertragung bleibt wie immer bestehen.
Wenn Leute von „Type Erasure“ sprechen, meinen sie genau das.Der Compiler fügt die Umwandlungen für Sie ein und „löscht“ dann die Tatsache, dass es sich um eine Liste von handeln soll Person nicht nur Object

Der Vorteil dieses Ansatzes besteht darin, dass alter Code, der Generika nicht versteht, sich nicht darum kümmern muss.Es handelt sich immer noch um das Gleiche ArrayList wie immer.Dies ist in der Java-Welt wichtiger, da sie das Kompilieren von Code unter Verwendung von Java 5 mit Generika unterstützen und ihn auf alten 1.4- oder früheren JVMs ausführen wollten, womit Microsoft sich bewusst nicht beschäftigt hat.

Der Nachteil ist der Geschwindigkeitseinbruch, den ich zuvor erwähnt habe, und auch, weil es keinen gibt ListOfPerson Pseudoklasse oder etwas Ähnliches, das in die .class-Dateien eingefügt wird, Code, der es später betrachtet (mit Reflektion, oder wenn Sie es aus einer anderen Sammlung ziehen, in die es konvertiert wurde). Object oder so weiter) kann in keiner Weise erkennen, dass es sich um eine Liste handeln soll, die nur enthält Person und nicht irgendeine andere Array-Liste.

Mit C++-Vorlagen können Sie so etwas deklarieren

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

Es sieht aus wie C#- und Java-Generika und wird das tun, was Sie denken, aber hinter den Kulissen passieren andere Dinge.

Es hat die meisten Gemeinsamkeiten mit C#-Generika, da es speziell erstellt wird pseudo-classes Anstatt die Typinformationen einfach wegzuwerfen, wie es Java tut, ist es ein ganz anderes Problem.

Sowohl C# als auch Java erzeugen eine Ausgabe, die für virtuelle Maschinen konzipiert ist.Wenn Sie Code schreiben, der a Person Klasse darin, in beiden Fällen einige Informationen über a Person class wird in die .dll- oder .class-Datei verschoben, und die JVM/CLR erledigt die Aufgaben damit.

C++ erzeugt rohen x86-Binärcode.Alles ist nicht ein Objekt, und es gibt keine zugrunde liegende virtuelle Maschine, die etwas über a wissen muss Person Klasse.Es gibt kein Boxing oder Unboxing und Funktionen müssen nicht zu Klassen oder irgendetwas anderem gehören.

Aus diesem Grund legt der C++-Compiler keine Einschränkungen hinsichtlich der Verwendung von Vorlagen fest – grundsätzlich können Sie jeden Code, den Sie manuell schreiben könnten, von Vorlagen für Sie schreiben lassen.
Das offensichtlichste Beispiel ist das Hinzufügen von Dingen:

In C# und Java muss das generische System wissen, welche Methoden für eine Klasse verfügbar sind, und diese an die virtuelle Maschine weitergeben.Die einzige Möglichkeit, dies festzustellen, besteht darin, entweder die eigentliche Klasse fest zu codieren oder Schnittstellen zu verwenden.Zum Beispiel:

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

Dieser Code lässt sich nicht in C# oder Java kompilieren, da er den Typ nicht kennt T stellt tatsächlich eine Methode namens Name() bereit.Das muss man sagen – in C# so:

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

Und dann müssen Sie sicherstellen, dass die Dinge, die Sie an addNames übergeben, die IHasName-Schnittstelle usw. implementieren.Die Java-Syntax ist anders (<T extends IHasName>), aber es weist die gleichen Probleme auf.

Der „klassische“ Fall für dieses Problem besteht darin, eine Funktion zu schreiben, die dies tut

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

Sie können diesen Code nicht wirklich schreiben, da es keine Möglichkeit gibt, eine Schnittstelle mit zu deklarieren + Methode darin.Du scheiterst.

C++ weist keines dieser Probleme auf.Dem Compiler ist die Weitergabe von Typen an VMs egal. Wenn beide Objekte über eine .Name()-Funktion verfügen, wird er kompiliert.Wenn sie es nicht tun, wird es nicht passieren.Einfach.

So, da haben Sie es :-)

Andere Tipps

C++ verwendet selten die „Generics“-Terminologie.Stattdessen wird das Wort „Vorlagen“ verwendet und ist zutreffender.Vorlagen beschreibt eines Technik um ein generisches Design zu erreichen.

C++-Vorlagen unterscheiden sich aus zwei Hauptgründen stark von dem, was sowohl C# als auch Java implementieren.Der erste Grund ist, dass C++-Vorlagen nicht nur Typargumente zur Kompilierungszeit, sondern auch Konstantwertargumente zur Kompilierungszeit zulassen:Vorlagen können als Ganzzahlen oder sogar Funktionssignaturen angegeben werden.Das bedeutet, dass Sie zur Kompilierungszeit einige ziemlich ungewöhnliche Dinge tun können, z.Berechnungen:

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;

Dieser Code nutzt auch das andere herausragende Merkmal von C++-Vorlagen, nämlich die Vorlagenspezialisierung.Der Code definiert eine Klassenvorlage, product das hat ein Wertargument.Außerdem wird eine Spezialisierung für diese Vorlage definiert, die immer dann verwendet wird, wenn das Argument den Wert 1 ergibt.Dadurch kann ich eine Rekursion über Vorlagendefinitionen definieren.Ich glaube, dass dies zuerst von entdeckt wurde Andrei Alexandrescu.

Die Vorlagenspezialisierung ist für C++ wichtig, da sie strukturelle Unterschiede in den Datenstrukturen ermöglicht.Vorlagen als Ganzes sind eine Möglichkeit, eine Schnittstelle typübergreifend zu vereinheitlichen.Obwohl dies wünschenswert ist, können innerhalb der Implementierung nicht alle Typen gleich behandelt werden.C++-Vorlagen berücksichtigen dies.Dies ist im Wesentlichen der gleiche Unterschied, den OOP zwischen Schnittstelle und Implementierung durch das Überschreiben virtueller Methoden macht.

C++-Vorlagen sind für das algorithmische Programmierparadigma von wesentlicher Bedeutung.Beispielsweise sind fast alle Algorithmen für Container als Funktionen definiert, die den Containertyp als Vorlagentyp akzeptieren und ihn einheitlich behandeln.Eigentlich stimmt das nicht ganz:C++ funktioniert nicht mit Containern, sondern mit Bereiche die durch zwei Iteratoren definiert werden, die auf den Anfang und hinter das Ende des Containers zeigen.Somit wird der gesamte Inhalt durch die Iteratoren umschrieben:Anfang <= Elemente < Ende.

Die Verwendung von Iteratoren anstelle von Containern ist nützlich, da sie die Bearbeitung von Teilen eines Containers statt des gesamten Containers ermöglicht.

Ein weiteres Unterscheidungsmerkmal von C++ ist die Möglichkeit von teilweise Spezialisierung für Klassenvorlagen.Dies hängt in gewisser Weise mit dem Mustervergleich bei Argumenten in Haskell und anderen funktionalen Sprachen zusammen.Betrachten wir zum Beispiel eine Klasse, die Elemente speichert:

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

Dies funktioniert für jeden Elementtyp.Nehmen wir jedoch an, dass wir Zeiger durch die Anwendung eines speziellen Tricks effizienter als andere Typen speichern können.Wir können dies tun, indem wir teilweise Spezialisiert auf alle Zeigertypen:

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

Wenn wir nun eine Containervorlage für einen Typ instanziieren, wird die entsprechende Definition verwendet:

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

Anders Hejlsberg selbst beschrieb hier die Unterschiede:Generika in C#, Java und C++".

Es gibt bereits viele gute Antworten Was Die Unterschiede sind, also lassen Sie mich eine etwas andere Perspektive geben und das hinzufügen Warum.

Wie bereits erläutert, besteht der Hauptunterschied darin Geben Sie Löschung ein, d.h.die Tatsache, dass der Java-Compiler die generischen Typen löscht und sie nicht im generierten Bytecode landen.Die Frage ist jedoch:Warum sollte das jemand tun?Es macht keinen Sinn!Oder doch?

Nun, was ist die Alternative?Wenn Sie keine Generika in der Sprache implementieren, wo Tun setzt du sie um?Und die Antwort lautet:in der virtuellen Maschine.Dadurch wird die Abwärtskompatibilität beeinträchtigt.

Mit der Typlöschung hingegen können Sie generische Clients mit nicht generischen Bibliotheken kombinieren.Mit anderen Worten:Code, der auf Java 5 kompiliert wurde, kann weiterhin auf Java 1.4 bereitgestellt werden.

Microsoft hat jedoch beschlossen, die Abwärtskompatibilität für Generika aufzuheben. Das ist warum .NET-Generika „besser“ sind als Java-Generika.

Natürlich sind Sun weder Idioten noch Feiglinge.Der Grund, warum sie sich „aufgeregt“ haben, war, dass Java deutlich älter und weiter verbreitet war als .NET, als sie Generika einführten.(Sie wurden in beiden Welten ungefähr zur gleichen Zeit eingeführt.) Die Aufhebung der Abwärtskompatibilität wäre ein großer Schmerz gewesen.

Noch anders ausgedrückt:In Java sind Generics ein Teil von Sprache (was bedeutet, dass sie gelten nur zu Java, nicht zu anderen Sprachen), in .NET sind sie Teil der Virtuelle Maschine (was bedeutet, dass sie gelten für alle Sprachen, nicht nur C# und Visual Basic.NET).

Vergleichen Sie dies mit .NET-Funktionen wie LINQ, Lambda-Ausdrücken, Inferenz lokaler Variablentypen, anonymen Typen und Ausdrucksbäumen:Das sind alle Sprache Merkmale.Deshalb gibt es subtile Unterschiede zwischen VB.NET und C#:Wenn diese Funktionen Teil der VM wären, wären sie identisch alle Sprachen.Aber die CLR hat sich nicht geändert:In .NET 3.5 SP1 ist es immer noch dasselbe wie in .NET 2.0.Sie können ein C#-Programm, das LINQ verwendet, mit dem .NET 3.5-Compiler kompilieren und es dennoch unter .NET 2.0 ausführen, vorausgesetzt, Sie verwenden keine .NET 3.5-Bibliotheken.Das würde nicht Arbeite mit Generika und .NET 1.1, aber es würde Arbeiten mit Java und Java 1.4.

Fortsetzung meines vorherigen Beitrags.

Vorlagen sind einer der Hauptgründe, warum C++ bei Intellisense so katastrophal scheitert, unabhängig von der verwendeten IDE.Aufgrund der Vorlagenspezialisierung kann die IDE nie wirklich sicher sein, ob ein bestimmtes Mitglied existiert oder nicht.Halten:

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

template <>
struct X<int> { };

typedef int my_int_type;

X<my_int_type> a;
a.|

Jetzt befindet sich der Cursor an der angegebenen Position und es ist für die IDE verdammt schwierig, an dieser Stelle zu sagen, ob und welche Mitglieder vorhanden sind a hat.Für andere Sprachen wäre das Parsen unkompliziert, aber für C++ ist im Vorfeld eine ganze Reihe von Auswertungen erforderlich.

Es wird schlimmer.Was ist, wenn my_int_type wurden auch in einer Klassenvorlage definiert?Jetzt würde sein Typ von einem anderen Typargument abhängen.Und hier versagen sogar Compiler.

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

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

Nach einigem Nachdenken würde ein Programmierer zu dem Schluss kommen, dass dieser Code mit dem oben genannten identisch ist: Y<int>::my_type beschließt, int, daher b sollte vom gleichen Typ sein wie a, Rechts?

Falsch.An dem Punkt, an dem der Compiler versucht, diese Anweisung aufzulösen, weiß er es nicht wirklich Y<int>::my_type noch!Daher weiß es nicht, dass es sich um einen Typ handelt.Es könnte auch etwas anderes sein, z.B.eine Memberfunktion oder ein Feld.Dies kann zu Mehrdeutigkeiten führen (im vorliegenden Fall jedoch nicht), daher schlägt der Compiler fehl.Wir müssen explizit sagen, dass wir uns auf einen Typnamen beziehen:

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

Jetzt wird der Code kompiliert.Um zu sehen, wie aus dieser Situation Mehrdeutigkeiten entstehen, betrachten Sie den folgenden Code:

Y<int>::my_type(123);

Diese Codeanweisung ist vollkommen gültig und weist C++ an, den Funktionsaufruf auszuführen Y<int>::my_type.Wie auch immer, wenn my_type keine Funktion, sondern ein Typ ist, wäre diese Anweisung dennoch gültig und würde eine spezielle Umwandlung (die Umwandlung im Funktionsstil) durchführen, bei der es sich häufig um einen Konstruktoraufruf handelt.Der Compiler kann nicht erkennen, was wir meinen, daher müssen wir hier eine eindeutige Unterscheidung treffen.

Sowohl Java als auch C# führten nach ihrer ersten Sprachveröffentlichung Generika ein.Es gibt jedoch Unterschiede in der Art und Weise, wie sich die Kernbibliotheken mit der Einführung von Generika verändert haben. Die Generika von C# sind nicht nur Compiler-Magie und so war es nicht möglich generieren bestehende Bibliotheksklassen ohne Beeinträchtigung der Abwärtskompatibilität.

Beispielsweise ist in Java das Vorhandene vorhanden Sammlungsrahmen War völlig generisch. Java verfügt weder über eine generische noch über eine ältere, nicht generische Version der Collections-Klassen. In mancher Hinsicht ist das viel sauberer – wenn Sie eine Sammlung in C# verwenden müssen, gibt es eigentlich kaum einen Grund, sich für die nicht-generische Version zu entscheiden, aber diese Legacy-Klassen bleiben bestehen und verstopfen die Landschaft.

Ein weiterer bemerkenswerter Unterschied sind die Enum-Klassen in Java und C#. Javas Enum hat diese etwas kompliziert aussehende Definition:

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

(Siehe Angelika Langers sehr deutlich Erklärung, warum genau das ist so.Im Wesentlichen bedeutet dies, dass Java typsicheren Zugriff von einem String auf seinen Enum-Wert gewähren kann:

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

Vergleichen Sie dies mit der C#-Version:

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

Da Enum bereits in C# existierte, bevor Generika in die Sprache eingeführt wurden, konnte die Definition nicht geändert werden, ohne den vorhandenen Code zu beschädigen.Daher verbleibt es wie Sammlungen in diesem Legacy-Zustand in den Kernbibliotheken.

11 Monate zu spät, aber ich denke, diese Frage ist für einige Java-Wildcard-Sachen bereit.

Dies ist eine syntaktische Funktion von Java.Angenommen, Sie haben eine Methode:

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

Angenommen, Sie müssen im Methodenkörper nicht auf den Typ T verweisen.Sie deklarieren einen Namen T und verwenden ihn dann nur einmal. Warum sollten Sie sich also einen Namen dafür ausdenken?Stattdessen können Sie schreiben:

public void Foo(Collection<?> thing)

Das Fragezeichen fordert den Compiler auf, so zu tun, als hätten Sie einen normalen benannten Typparameter deklariert, der an dieser Stelle nur einmal vorkommen muss.

Es gibt nichts, was Sie mit Platzhaltern tun können, was Sie nicht auch mit einem benannten Typparameter tun können (wie diese Dinge in C++ und C# immer gemacht werden).

Wikipedia bietet großartige Artikel, in denen beide verglichen werden Java/C#-Generika Und Java-Generika/C++ Vorlagen.Der Hauptartikel über Generika Scheint etwas überladen zu sein, enthält aber einige gute Informationen.

Die größte Beschwerde ist die Löschung von Schriftarten.Dabei werden Generika zur Laufzeit nicht erzwungen. Hier ist ein Link zu einigen Sun-Dokumenten zu diesem Thema.

Generika werden nach Typenlöschungen implementiert:Generische Typinformationen sind nur zur Kompilierungszeit vorhanden, wonach sie vom Compiler gelöscht werden.

C++-Vorlagen sind tatsächlich viel leistungsfähiger als ihre C#- und Java-Gegenstücke, da sie zur Kompilierungszeit ausgewertet werden und Spezialisierung unterstützen.Dies ermöglicht Template-Metaprogrammierung und macht den C++-Compiler äquivalent zu einer Turing-Maschine (d. h.Während des Kompilierungsprozesses können Sie alles berechnen, was mit einer Turing-Maschine berechenbar ist.

In Java gibt es Generika nur auf Compiler-Ebene, sodass Sie Folgendes erhalten:

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

Beachten Sie, dass der Typ von „a“ eine Array-Liste und keine Liste von Zeichenfolgen ist.Der Typ einer Liste mit Bananen würde also () einer Liste mit Affen entsprechen.

Sozusagen.

Es sieht so aus, als gäbe es neben anderen sehr interessanten Vorschlägen auch einen zur Verfeinerung von Generika und zur Aufhebung der Abwärtskompatibilität:

Derzeit werden Generika mithilfe der Löschung implementiert, was bedeutet, dass die generischen Typinformationen zur Laufzeit nicht verfügbar sind, was eine Art Code schwierig macht.Generika wurden auf diese Weise implementiert, um die Rückwärtskompatibilität mit älteren nicht generischen Code zu unterstützen.Refed Generics würde die zur Laufzeit verfügbaren generischen Informationen zur Verfügung stellen, wodurch der nicht generische Code des älteren generischen Codes verstoßen würde.Neal Gafter hat jedoch vorgeschlagen, die Typen nur dann neu zu machen, wenn sie angegeben sind, um nicht nach rückwärts zu sein.

bei Alex Millers Artikel über Java 7-Vorschläge

Hinweis:Ich habe nicht genug Anlass für einen Kommentar, Sie können dies also gerne als Kommentar zur entsprechenden Antwort verschieben.

Entgegen der landläufigen Meinung, von der ich nie verstehe, woher sie kommt, hat .net echte Generika implementiert, ohne die Abwärtskompatibilität zu beeinträchtigen, und dafür wurden explizite Anstrengungen unternommen.Sie müssen Ihren nicht generischen .net 1.0-Code nicht in generischen Code umwandeln, nur um ihn in .net 2.0 zu verwenden.Sowohl die generischen als auch die nicht-generischen Listen sind in .Net Framework 2.0 auch bis 4.0 weiterhin verfügbar, und zwar ausschließlich aus Gründen der Abwärtskompatibilität.Daher funktionieren alte Codes, die noch nicht generische ArrayList verwendeten, weiterhin und verwenden dieselbe ArrayList-Klasse wie zuvor.Die Abwärtskompatibilität des Codes bleibt seit 1.0 bis jetzt immer erhalten ...Selbst in .net 4.0 müssen Sie also immer noch die Option haben, jede nicht generische Klasse aus 1.0 BCL zu verwenden, wenn Sie sich dafür entscheiden.

Daher glaube ich nicht, dass Java die Abwärtskompatibilität unterbrechen muss, um echte Generika zu unterstützen.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top