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

Ich benutze meistens Java und Generika sind relativ neu. Ich lese weiter, dass Java die falsche Entscheidung getroffen hat oder dass .NET bessere Implementierungen etc. hat.

Also, was sind die Hauptunterschiede zwischen C ++, C #, Java in Generics? Vor- / Nachteile von jedem?

Ich werde meine Stimme dem Lärm hinzufügen und einen Stich machen, um Dinge klar zu machen:

Mit C # Generics können Sie so etwas deklarieren.

List foo = new List(); 

und dann wird der Compiler Sie daran hindern, Dinge, die keine Person in die Liste zu legen.
Hinter den Kulissen fügt der C # -Compiler lediglich List in die .NET-DLL-Datei ein, aber zur Laufzeit geht der JIT-Compiler und erstellt einen neuen Code, als hätten Sie eine spezielle Listenklasse geschrieben, die nur Leute enthält wie ListOfPerson .

Der Vorteil ist, dass es wirklich schnell macht. Es gibt kein Casting oder irgendwas anderes, und da die DLL die Information enthält, dass dies eine Liste von Person , kann anderer Code, der es später mit Reflektion betrachtet, erkennen, dass es Person Objekte enthält (also erhalten Sie Intellisense usw.).

Der Nachteil davon ist, dass der alte C # 1.0- und 1.1-Code (bevor sie Generics hinzufügten) diese neuen List nicht verstehen, also müssen Sie die Dinge manuell zurück in die einfache alte List konvertieren, um mit ihnen zusammenzuarbeiten. Dies ist kein großes Problem, da der C # 2.0-Binärcode nicht abwärtskompatibel ist. Das einzige Mal, dass dies jemals passieren wird, ist, wenn Sie einen alten C # 1.0 / 1.1-Code auf C # 2.0 aktualisieren

Mit Java Generics können Sie so etwas deklarieren.

 ArrayList foo = new ArrayList(); 

Auf der Oberfläche sieht es genauso aus, und es ist irgendwie so. Der Compiler verhindert auch, dass Sie Dinge, die keine Person in die Liste einfügen.

Der Unterschied ist, was hinter den Kulissen passiert. Im Gegensatz zu C # erstellt Java keine spezielle ListOfPerson – es verwendet nur die einfache alte ArrayList die immer in Java war. Wenn Sie Dinge aus dem Array Person p = (Person)foo.get(1); , die übliche Person p = (Person)foo.get(1); Casting-Dance muss noch getan werden. Der Compiler erspart Ihnen das Drücken der Tasten, aber die Geschwindigkeit, mit der das Casting ausgeführt wird, ist immer noch so, wie es immer war.
Wenn Leute “Type Erasure” erwähnen, sprechen sie davon. Der Compiler fügt die Umwandlungen für Sie ein und löscht dann die Tatsache, dass es eine Liste von Person not just 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 dieselbe alte ArrayList wie immer. Dies ist in der Java-Welt wichtiger, da sie den Code mit Java 5 mit Generics kompilieren wollten und ihn auf alten 1.4 oder früheren JVMs laufen ließen, auf die Microsoft bewusst verzichtet hatte.

Der Nachteil ist der Geschwindigkeitshit, den ich vorher erwähnte, und auch, weil es keine ListOfPerson Pseudo-class oder etwas ähnliches in die .class-Dateien gibt, Code, der es später betrachtet (mit Reflektion oder wenn Sie es aus einem anderen ziehen) Sammlung, in die es in Object konvertiert wurde usw.) kann in keiner Weise feststellen, dass es sich um eine Liste handelt, die nur Person und nicht irgendeine andere Array-Liste enthält.

Mit C ++ Templates können Sie so etwas deklarieren

 std::list* foo = new std::list(); 

Es sieht aus wie C # und Java Generics, und es wird tun, was Sie denken, dass es tun sollte, aber hinter den Kulissen passieren verschiedene Dinge.

Es hat die meisten Gemeinsamkeiten mit C # -Generika, da es spezielle pseudo-classes und nicht nur die Typinformationen wegwirft, wie Java es tut, aber es ist ein ganz anderer Kessel Fisch.

Sowohl C # als auch Java erzeugen Ausgaben, die für virtuelle Maschinen ausgelegt sind. Wenn Sie einen Code schreiben, der eine Person class enthält, werden in beiden Fällen einige Informationen über eine Person class in die .dll- oder .class-Datei geschrieben, und die JVM / CLR wird damit arbeiten.

C ++ erzeugt rohen x86-Binärcode. Alles ist kein Objekt und es gibt keine zugrunde liegende virtuelle Maschine, die etwas über eine Person class wissen Person . Es gibt kein Boxing oder Unboxing, und functionen müssen nicht zu classn oder gar irgendetwas gehören.

Aus diesem Grund gibt es beim C ++ – Compiler keine Einschränkungen hinsichtlich der Möglichkeiten, die Sie mit Templates erreichen können. Im Grunde kann jeder Code, den Sie manuell schreiben könnten, Vorlagen erhalten, die Sie schreiben können.
Das offensichtlichste Beispiel ist das Hinzufügen von Dingen:

In C # und Java muss das Generikasystem wissen, welche Methoden für eine class verfügbar sind, und diese muss an die virtuelle Maschine weitergegeben werden. Die einzige Möglichkeit, dies zu erkennen, besteht darin, entweder die tatsächliche class fest zu codieren oder Schnittstellen zu verwenden. Beispielsweise:

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

Dieser Code wird nicht in C # oder Java kompiliert, weil er nicht weiß, dass der Typ T tatsächlich eine Methode namens Name () bereitstellt. Sie müssen es sagen – in C # wie folgt:

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

Und dann müssen Sie sicherstellen, dass die Dinge, die Sie an addNames übergeben, die IHasName-Schnittstelle und so weiter implementieren. Die Java-Syntax ist anders ( ), leidet aber unter den gleichen Problemen.

Der “klassische” Fall für dieses Problem ist der Versuch, eine function zu schreiben, die dies tut

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

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

C ++ leidet unter keinem dieser Probleme. Der Compiler kümmert sich nicht darum, Typen an VMs zu übergeben – wenn beide Objekte eine .Name () – function haben, wird sie kompiliert. Wenn sie es nicht tun, wird es nicht. Einfach.

Also, da hast du es 🙂

C ++ verwendet selten die Terminologie “Generika”. Stattdessen wird das Wort “Vorlagen” verwendet und ist genauer. Vorlagen beschreiben eine Technik , um ein generisches Design zu erreichen.

C ++ – Vorlagen unterscheiden sich grundlegend von dem, was sowohl C # als auch Java aus zwei Hauptgründen implementieren. Der erste Grund ist, dass C ++ – Templates nicht nur Typargumente zum Kompilieren zulassen, sondern auch Argumente zum Const-Wert zum Kompilieren: Templates können als Ganzzahlen oder sogar als functionssignaturen angegeben werden. Das bedeutet, dass Sie zur Kompilierzeit einige ziemlich funky Sachen machen können, zB Berechnungen:

 template  struct product { static unsigned int const VALUE = N * product::VALUE; }; template <> struct product<1> { static unsigned int const VALUE = 1; }; // Usage: unsigned int const p5 = product<5>::VALUE; 

Dieser Code verwendet auch das andere unterscheidbare Feature von C ++ – Vorlagen, nämlich Template-Spezialisierung. Der Code definiert eine classnvorlage, ein product mit einem Wertargument. Es definiert auch eine Spezialisierung für diese Vorlage, die immer dann verwendet wird, wenn das Argument 1 ergibt. Dadurch kann eine Rekursion über Template-Definitionen definiert werden. Ich glaube, dass dies zuerst von Andrei Alexandrescu entdeckt wurde.

Template-Spezialisierung ist wichtig für C ++, da es strukturelle Unterschiede in Datenstrukturen erlaubt. Templates als Ganzes ist ein Mittel, um eine Schnittstelle zwischen Typen zu vereinheitlichen. Obwohl dies wünschenswert ist, können jedoch alle Arten innerhalb der Implementierung nicht gleich behandelt werden. C ++ – Vorlagen berücksichtigen dies. Dies ist der gleiche Unterschied, den OOP zwischen Schnittstelle und Implementierung mit dem Überschreiben virtueller Methoden macht.

C ++ – Vorlagen sind essentiell für das Paradigma der algorithmischen Programmierung. Beispielsweise sind fast alle Algorithmen für Container als functionen definiert, die den Containertyp als Schablonentyp akzeptieren und einheitlich behandeln. Eigentlich ist das nicht ganz richtig: C ++ arbeitet nicht an Containern, sondern an Bereichen , die durch zwei Iteratoren definiert sind, die auf den Anfang und hinter das Ende des Containers zeigen. Daher wird der gesamte Inhalt von den Iteratoren umschrieben: begin < = elements

Es ist nützlich, Iteratoren anstelle von Containern zu verwenden, da damit Teile eines Containers anstatt des gesamten Containers bearbeitet werden können.

Ein weiteres Unterscheidungsmerkmal von C ++ ist die Möglichkeit einer teilweisen Spezialisierung für classnvorlagen. Dies hängt etwas mit dem Musterabgleich bei Argumenten in Haskell und anderen funktionalen Sprachen zusammen. Betrachten wir zum Beispiel eine class, die Elemente speichert:

 template  class Store { … }; // (1) 

Dies funktioniert für jeden Elementtyp. Aber lassen Sie uns sagen, dass wir pointers effizienter als andere Typen speichern können, indem Sie einen speziellen Trick anwenden. Wir können dies tun, indem wir uns für alle pointerstypen teilweise spezialisieren:

 template  class Store { … }; // (2) 

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

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

Anders Hejlsberg selbst hat hier die Unterschiede beschrieben: ” Generics in C #, Java und C ++ “.

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

Wie bereits erläutert, besteht der Hauptunterschied in der Typlöschung , dh in der Tatsache, dass der Java-Compiler die generischen Typen löscht und nicht im generierten Bytecode endet. Die Frage ist jedoch: Warum sollte jemand das tun? Es ergibt keinen Sinn! Oder tut es?

Nun, was ist die Alternative? Wenn Sie Generika nicht in der Sprache implementieren, wo implementieren Sie sie? Und die Antwort ist: in der virtuellen Maschine. Das bricht Rückwärtskompatibilität.

Mit Type Erasure können Sie dagegen generische Clients mit nicht-generischen Bibliotheken mischen. Mit anderen Worten: Code, der auf Java 5 kompiliert wurde, kann weiterhin auf Java 1.4 bereitgestellt werden.

Microsoft entschied sich jedoch, die Rückwärtskompatibilität für Generika zu brechen. Deshalb sind .NET Generics “besser” als Java Generics.

Natürlich sind Sun keine Idioten oder Feiglinge. Der Grund, warum sie “herausgepickt” haben, war, dass Java wesentlich älter und weiter verbreitet war als .NET, als sie Generika einführten. (Sie wurden ungefähr zur gleichen Zeit in beiden Welten eingeführt.) Die Rückwärtskompatibilität wäre ein großer Schmerz gewesen.

Anders ausgedrückt: In Java sind Generika ein Teil der Sprache (was bedeutet, dass sie nur für Java gelten, nicht für andere Sprachen), in .NET sind sie Teil der virtuellen Maschine (was bedeutet, dass sie für alle Sprachen gelten, nicht für nur C # und Visual Basic.NET).

Vergleichen Sie dies mit .NET-functionen wie LINQ, Lambda-Ausdrücken, lokalen Variablen-Typ-Inferenzen, anonymen Typen und Ausdrucksbäumen: das sind alles Sprachfeatures . Aus diesem Grund gibt es kleine Unterschiede zwischen VB.NET und C #: Wenn diese functionen Teil der VM wären, wären sie in allen Sprachen gleich. 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 kompilieren, das LINQ mit dem .NET 3.5-Compiler verwendet, und es dennoch auf .NET 2.0 ausführen, sofern Sie keine .NET 3.5-Bibliotheken verwenden. Das würde mit Generics und .NET 1.1 nicht funktionieren, aber es würde mit Java und Java 1.4 funktionieren.

Follow-up zu meinem vorherigen Beitrag.

Templates sind einer der Hauptgründe, warum C ++ bei Intellisense so abnormal ausfällt, unabhängig von der verwendeten IDE. Aufgrund der Template-Spezialisierung kann die IDE niemals wirklich sicher sein, ob ein bestimmtes Mitglied existiert oder nicht. Erwägen:

 template  struct X { void foo() { } }; template <> struct X { }; typedef int my_int_type; X a; a.| 

Jetzt befindet sich der Cursor an der angegebenen Position und es ist verdammt schwer für die IDE zu sagen, ob und was Mitglieder a hat. Für andere Sprachen wäre das Parsen einfach, aber für C ++ ist vorher eine gewisse Bewertung erforderlich.

Es wird schlimmer. Was wäre, wenn my_int_type auch in einer classnvorlage definiert wäre? Jetzt hängt sein Typ von einem anderen Typ-Argument ab. Und hier versagen sogar Compiler.

 template  struct Y { typedef T my_type; }; X::my_type> b; 

Nach ein bisschen Nachdenken würde ein Programmierer schlussfolgern, dass dieser Code derselbe ist wie oben: Y::my_type in int , daher sollte b derselbe Typ wie a , oder?

Falsch. An dem Punkt, an dem der Compiler versucht, diese statement aufzulösen, kennt er Y::my_type noch nicht! Daher weiß es nicht, dass dies ein Typ ist. Es könnte etwas anderes sein, zB eine Mitgliedsfunktion oder ein Feld. Dies kann zu Mehrdeutigkeiten führen (wenn auch nicht im vorliegenden Fall), daher schlägt der Compiler fehl. Wir müssen es ausdrücklich sagen, dass wir uns auf einen Typnamen beziehen:

 X::my_type> b; 

Jetzt kompiliert der Code. Beachten Sie den folgenden Code, um zu sehen, wie sich Mehrdeutigkeiten aus dieser Situation ergeben:

 Y::my_type(123); 

Diese Code-statement ist absolut gültig und teilt C ++ mit, den functionsaufruf an Y::my_type . Wenn my_type jedoch keine function, sondern ein Typ ist, wäre diese statement immer noch gültig und führt eine spezielle my_type aus (die functionsargumentation), bei der es sich oft um einen Konstruktoraufruf handelt. Der Compiler kann nicht sagen, was wir meinen, also müssen wir hier disambiguieren.

Sowohl Java als auch C # haben Generika nach ihrer ersten Sprachversion eingeführt. Es gibt jedoch Unterschiede in der Art und Weise, wie sich die corebibliotheken bei der Einführung von Generika verändert haben. C # ‘s Generics sind nicht nur Compiler-Magie und so war es nicht möglich, existierende Bibliotheksklassen zu generalisieren , ohne die Abwärtskompatibilität zu beeinträchtigen .

Zum Beispiel wurde in Java das vorhandene Collections Framework vollständig generalisiert . Java verfügt nicht sowohl über eine allgemeine als auch über eine ältere nicht generische Version der Auflistungsklassen. In mancher Hinsicht ist dies viel sauberer – wenn Sie eine Sammlung in C # verwenden müssen, gibt es wirklich wenig Grund, mit der nicht-generischen Version zu gehen, aber diese alten classn bleiben an ihrem Platz und überfluten die Landschaft.

Ein weiterer bemerkenswerter Unterschied ist die Enum-classn in Java und C #. Java’s Enum hat diese etwas gewunden aussehende Definition:

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

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

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

Vergleichen Sie dies mit C # ‘s 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 bestehenden Code zu unterbrechen. Wie Sammlungen verbleibt es in den corebibliotheken in diesem Legacy-Zustand.

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

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

 public  void Foo(Collection 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 einen Namen dafür denken? Stattdessen können Sie schreiben:

 public void Foo(Collection< ?> thing) 

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

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

Wikipedia hat großartige Zuschreibungen, die sowohl Java / C # -Generika als auch Java-Generika / C ++ – Vorlagen vergleichen. Der Hauptartikel über Generics scheint ein bisschen überladen, aber es hat einige gute Informationen drin.

Die größte Beschwerde ist das Löschen von Typen. Generics werden zur Laufzeit nicht erzwungen. Hier finden Sie einen Link zu einigen Sun-Dokumenten zum Thema .

Generika werden durch Typlöschen implementiert: generische Typinformationen sind nur zur Kompilierzeit vorhanden, nach denen sie vom Compiler gelöscht werden.

C ++ – Vorlagen sind tatsächlich viel leistungsstärker als ihre C # – und Java-Gegenstücke, da sie zur Kompilierzeit ausgewertet werden und Spezialisierung unterstützen. Dies ermöglicht Template Meta-Programming und macht den C ++ – Compiler äquivalent zu einer Turing-Maschine (dh während des Kompilierprozesses können Sie alles berechnen, was mit einer Turing-Maschine berechenbar ist).

In Java sind Generics nur Compiler-Level. Sie erhalten also:

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

Beachten Sie, dass der Typ von ‘a’ eine Array-Liste ist, keine Liste von Strings. Der Typ einer Liste von Bananen wäre also gleich () eine Liste von Affen.

Sozusagen.

Es scheint, neben anderen sehr interessanten Vorschlägen, gibt es eine über die Veredelung von Generika und die Rückwärtskompatibilität:

Gegenwärtig werden Generika durch Löschen implementiert, was bedeutet, dass die generische Typinformation zur Laufzeit nicht verfügbar ist, was eine Art von Code schwer zu schreiben macht. Generics wurden auf diese Weise implementiert, um die Rückwärtskompatibilität mit älteren nicht-generischen Codes zu unterstützen. Vereinheitlichte Generika würden die Informationen des generischen Typs zur Laufzeit verfügbar machen, wodurch nicht-generischer Standardcode außer Kraft gesetzt würde. Neal Gafter hat jedoch vorgeschlagen, Typen nur dann verifizierbar zu machen, wenn sie spezifiziert sind, um die Rückwärtskompatibilität nicht zu zerstören.

in Alex Millers Artikel über Java 7 Vorschläge

NB: Ich habe nicht genug Punkt, um zu kommentieren, also zögern Sie nicht, dies als Kommentar zur entsprechenden Antwort zu bewegen.

Entgegen der landläufigen Meinung, die ich nie verstehen konnte, woher sie kam, implementierte .net echte Generika, ohne die Rückwärtskompatibilität zu zerstören, und sie gaben sich explizite Mühe dafür. Sie müssen Ihren nicht generischen .net 1.0-Code nicht in Generics ändern, um in .net 2.0 verwendet zu werden. Sowohl die generischen als auch die nicht-generischen Listen sind in .NET Framework 2.0 sogar bis 4.0 verfügbar, und zwar ausschließlich aus Gründen der Rückwärtskompatibilität. Daher funktionieren alte Codes, die immer noch nicht-generische ArrayList verwenden, immer noch und verwenden dieselbe ArrayList-class wie zuvor. Rückwärtskompatibilität wird seit 1.0 bis jetzt immer beibehalten … Also sogar in .net 4.0, müssen Sie noch Option, jede Nicht-Generikenklasse von 1.0 BCL zu verwenden, wenn Sie sich dafür entscheiden.

Also glaube ich nicht, dass Java die Rückwärtskompatibilität brechen muss, um echte Generika zu unterstützen.