Antworten:
Ich werde dem Geräusch meine Stimme hinzufügen und versuchen, die Dinge klar zu machen:
List<Person> foo = new List<Person>();
und dann verhindert der Compiler, dass Sie Dinge einfügen, die nicht Person
in der Liste enthalten sind.
Hinter den Kulissen List<Person>
fügt der C # -Compiler nur die .NET-DLL-Datei ein, aber zur Laufzeit erstellt der JIT-Compiler einen neuen Code, als hätten Sie eine spezielle Listenklasse geschrieben, die nur Personen enthält - so etwas wie ListOfPerson
.
Dies hat den Vorteil, dass es sehr schnell geht. Es gibt kein Casting oder andere Dinge, und da die DLL die Informationen enthält, von denen dies eine Liste ist Person
, kann anderer Code, der sie später mithilfe von Reflection betrachtet, erkennen, dass sie Person
Objekte enthält (so dass Sie Intellisense erhalten und so weiter).
Der Nachteil dabei ist, dass alter C # 1.0- und 1.1-Code (bevor Generika hinzugefügt wurden) diese neuen nicht versteht. List<something>
Sie müssen die Dinge also manuell wieder in einfache alte konvertieren List
, um mit ihnen zusammenarbeiten zu können. Dies ist kein so großes Problem, da der C # 2.0-Binärcode nicht abwärtskompatibel ist. Dies kann nur passieren, wenn Sie einen alten C # 1.0 / 1.1-Code auf C # 2.0 aktualisieren
ArrayList<Person> foo = new ArrayList<Person>();
An der Oberfläche sieht es genauso aus und ist es auch. Der Compiler verhindert auch, dass Sie Dinge einfügen, die nicht Person
in der Liste enthalten sind.
Der Unterschied ist, was hinter den Kulissen passiert. Im Gegensatz zu C # erstellt Java kein spezielles Programm ListOfPerson
- es verwendet nur das einfache alte, ArrayList
das immer in Java verwendet wurde. Wenn Sie Dinge aus dem Array herausholen, muss der übliche Person p = (Person)foo.get(1);
Casting-Tanz noch durchgeführt werden. Der Compiler speichert Ihnen die Tastendrücke, aber der Geschwindigkeitstreffer / das Casting erfolgt immer noch so, wie es immer war.
Wenn Leute "Type Erasure" erwähnen, ist dies das, worüber sie sprechen. Der Compiler fügt die Casts für Sie ein und "löscht" dann die Tatsache, dass es sich Person
nicht nur um eine Liste handeltObject
Der Vorteil dieses Ansatzes ist, dass alter Code, der Generika nicht versteht, sich nicht darum kümmern muss. Es geht immer noch um das gleiche alte ArrayList
wie immer. Dies ist in der Java-Welt wichtiger, da sie das Kompilieren von Code mit Java 5 mit Generika unterstützen und auf alten 1.4- oder früheren JVMs ausführen wollten, mit denen sich Microsoft bewusst nicht befasst hat.
Der Nachteil ist der Geschwindigkeitstreffer, den ich zuvor erwähnt habe, und auch, weil ListOfPerson
in den .class-Dateien keine Pseudoklasse oder ähnliches enthalten ist, Code, der sie später betrachtet (mit Reflexion oder wenn Sie sie aus einer anderen Sammlung ziehen) wo es konvertiert wurde Object
oder so weiter) kann in keiner Weise sagen, dass es eine Liste sein soll, die nur Person
und nicht irgendeine andere Array-Liste enthält.
std::list<Person>* foo = new std::list<Person>();
Es sieht aus wie C # - und Java-Generika und es wird das tun, was Sie denken, aber hinter den Kulissen passieren verschiedene Dinge.
Es hat das meiste mit C # -Generika gemeinsam, da es etwas Besonderes erstellt, pseudo-classes
anstatt nur die Typinformationen wegzuwerfen, wie es Java tut, aber es ist ein ganz anderer Fischkessel.
Sowohl C # als auch Java erzeugen eine Ausgabe, die für virtuelle Maschinen entwickelt wurde. Wenn Sie einen Code schreiben, der eine Person
Klasse enthält, werden in beiden Fällen einige Informationen zu einer Person
Klasse in die DLL- oder .class-Datei aufgenommen, und die JVM / CLR erledigt dies.
C ++ erzeugt rohen x86-Binärcode. Alles ist kein Objekt und es gibt keine zugrunde liegende virtuelle Maschine, die etwas über eine Person
Klasse wissen muss . Es gibt kein Boxen oder Unboxing und Funktionen müssen nicht zu Klassen gehören oder so.
Aus diesem Grund schränkt der C ++ - Compiler die Möglichkeiten von Vorlagen nicht ein. Grundsätzlich können Sie bei jedem Code, den Sie manuell schreiben können, 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 dies muss an die virtuelle Maschine weitergegeben werden. Die einzige Möglichkeit, dies festzustellen, besteht darin, entweder die tatsächliche Klasse fest zu codieren oder Schnittstellen zu verwenden. Beispielsweise:
string addNames<T>( T first, T second ) { return first.Name() + second.Name(); }
Dieser Code wird nicht in C # oder Java kompiliert, da 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>( T first, T second ) where T : IHasName { .... }
Und dann müssen Sie sicherstellen, dass die Dinge, die Sie an addNames übergeben, die IHasName-Schnittstelle implementieren und so weiter. Die Java-Syntax ist unterschiedlich ( <T extends IHasName>
), weist jedoch dieselben Probleme auf.
Der "klassische" Fall für dieses Problem ist der Versuch, eine Funktion zu schreiben, die dies tut
string addNames<T>( T first, T second ) { return first + second; }
Sie können diesen Code nicht schreiben, da es keine Möglichkeit gibt, eine Schnittstelle mit der darin enthaltenen +
Methode zu deklarieren . Du scheiterst.
C ++ leidet unter keinem dieser Probleme. Der Compiler kümmert sich nicht darum, Typen an VMs weiterzugeben. Wenn beide Objekte über die Funktion .Name () verfügen, wird er kompiliert. Wenn nicht, wird es nicht. Einfach.
Also, da hast du es :-)
int addNames<T>( T first, T second ) { return first + second; }
in C # schreiben können . Der generische Typ kann auf eine Klasse anstelle einer Schnittstelle beschränkt werden, und es gibt eine Möglichkeit, eine Klasse mit dem +
Operator darin zu deklarieren .
C ++ verwendet selten die Terminologie "Generika". Stattdessen wird das Wort "Vorlagen" verwendet und ist genauer. Vorlagen beschreiben eine Technik , um ein generisches Design zu erzielen.
C ++ - Vorlagen unterscheiden sich aus zwei Hauptgründen stark von der Implementierung von C # und Java. Der erste Grund ist, dass C ++ - Vorlagen nicht nur Argumente vom Typ zur Kompilierungszeit zulassen, sondern auch Argumente zum Konstantenwert zur Kompilierungszeit: Vorlagen können als Ganzzahlen oder sogar als Funktionssignaturen angegeben werden. Dies bedeutet, dass Sie zur Kompilierungszeit einige ziemlich funky Dinge tun können, z. B. 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 verwendet auch die andere herausragende Funktion von C ++ - Vorlagen, nämlich die Vorlagenspezialisierung. Der Code definiert eine Klassenvorlage product
mit einem Wertargument. Außerdem wird eine Spezialisierung für diese Vorlage definiert, die immer dann verwendet wird, wenn das Argument 1 ergibt. Auf diese Weise kann ich eine Rekursion über Vorlagendefinitionen definieren. Ich glaube, dass dies zuerst von Andrei Alexandrescu entdeckt wurde .
Die Vorlagenspezialisierung ist für C ++ wichtig, da sie strukturelle Unterschiede in den Datenstrukturen berücksichtigt. Vorlagen als Ganzes sind ein Mittel zur Vereinheitlichung einer Schnittstelle zwischen verschiedenen Typen. Obwohl dies wünschenswert ist, können nicht alle Typen innerhalb der Implementierung gleich behandelt werden. C ++ - Vorlagen berücksichtigen dies. Dies ist fast der gleiche Unterschied, den OOP zwischen Schnittstelle und Implementierung beim Ü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 einheitlich behandeln. Eigentlich ist das nicht ganz richtig: C ++ funktioniert nicht für Container, sondern für Bereiche , die von zwei Iteratoren definiert werden und auf den Anfang und hinter das Ende des Containers zeigen. Somit wird der gesamte Inhalt von den Iteratoren umschrieben: begin <= elements <end.
Die Verwendung von Iteratoren anstelle von Containern ist nützlich, da damit Teile eines Containers anstatt insgesamt bearbeitet werden können.
Ein weiteres Unterscheidungsmerkmal von C ++ ist die Möglichkeit einer teilweisen Spezialisierung für Klassenvorlagen. Dies hängt etwas mit dem Mustervergleich für Argumente 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. Angenommen, wir können Zeiger effizienter als andere Typen speichern, indem wir einen speziellen Trick anwenden. Wir können dies tun, indem wir uns teilweise auf alle Zeigertypen spezialisieren:
template <typename T>
class Store<T*> { … }; // (2)
Wenn wir jetzt 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 " Generics in C #, Java und C ++ ".
Es gibt bereits viele gute Antworten auf , was die Unterschiede sind, so lassen Sie mich eine etwas andere Perspektive geben und fügen Sie das warum .
Wie bereits erläutert, besteht der Hauptunterschied in der Typlöschung , dh der Tatsache, dass der Java-Compiler die generischen Typen löscht und sie nicht im generierten Bytecode landen. Die Frage ist jedoch: Warum sollte jemand das tun? Es macht keinen Sinn! Oder doch?
Was ist die Alternative? Wenn Sie keine Generika in der Sprache implementieren, wo sie implementieren Sie sie? Und die Antwort lautet: in der virtuellen Maschine. Was die Abwärtskompatibilität bricht.
Mit Type Erasure hingegen können Sie 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 hat jedoch beschlossen, die Abwärtskompatibilität für Generika zu unterbrechen. Deshalb sind .NET-Generika "besser" als Java-Generika.
Natürlich sind Sun keine Idioten oder Feiglinge. Der Grund, warum sie "durchgeknallt" waren, war, dass Java bei der Einführung von Generika deutlich älter und weiter verbreitet war als .NET. (Sie wurden in beiden Welten ungefähr zur gleichen Zeit eingeführt.) Die Abwärtskompatibilität zu brechen, wäre ein großer Schmerz gewesen.
Anders ausgedrückt: In Java sind Generika Teil der Sprache (dh sie gelten nur für Java, nicht für andere Sprachen), in .NET sind sie Teil der virtuellen Maschine (dh sie gelten nicht für alle Sprachen) nur C # und Visual Basic.NET).
Vergleichen Sie dies mit .NET wie LINQ verfügt, Lambda - Ausdrücke, lokale Variable Typinferenz, anonyme Typen und Ausdrucksbäume: Das sind alle Sprachfunktionen. Aus diesem Grund gibt es subtile Unterschiede zwischen VB.NET und C #: Wenn diese Funktionen Teil der VM wären, wären sie in allen Sprachen gleich. Die CLR hat sich jedoch nicht geändert: In .NET 3.5 SP1 ist sie immer noch dieselbe 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, sofern Sie keine .NET 3.5-Bibliotheken verwenden. Das würde nicht mit Generika und .NET 1.1 funktionieren , aber es würde mit Java und Java 1.4 funktionieren.
ArrayList<T>
kann mit einem (versteckten) statisch als neuer intern genannten Art emittiert wird Class<T>
Feld. Solange die neue Version von generic lib mit mehr als 1,5 Byte Code bereitgestellt wurde, kann sie auf 1.4-JVMs ausgeführt werden.
Follow-up zu meinem vorherigen Beitrag.
Vorlagen sind einer der Hauptgründe, warum C ++ bei Intellisense unabhängig von der verwendeten IDE so miserabel ausfällt. Aufgrund der Vorlagenspezialisierung kann die IDE nie wirklich sicher sein, ob ein bestimmtes Mitglied existiert oder nicht. Erwägen:
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 verdammt schwer für die IDE zu diesem Zeitpunkt zu sagen, ob und was Mitglieder a
haben. Für andere Sprachen wäre das Parsen unkompliziert, aber für C ++ ist vorher einiges an Evaluierung erforderlich.
Es wird schlimmer. Was wäre, wenn my_int_type
sie auch in einer Klassenvorlage definiert wären ? Jetzt würde sein Typ von einem anderen Typargument abhängen. Und hier scheitern 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 derselbe wie der oben genannte ist: Er Y<int>::my_type
löst sich auf int
und b
sollte daher vom selben Typ sein wie a
, richtig?
Falsch. An dem Punkt, an dem der Compiler versucht, diese Anweisung aufzulösen, weiß er es noch Y<int>::my_type
nicht! Daher weiß es nicht, dass dies ein Typ ist. Es könnte etwas anderes sein, z. B. eine Mitgliedsfunktion oder ein Feld. Dies kann zu Unklarheiten führen (im vorliegenden Fall jedoch nicht), weshalb der Compiler fehlschlägt. Wir müssen ausdrücklich darauf hinweisen, dass wir uns auf einen Typnamen beziehen:
X<typename Y<int>::my_type> b;
Jetzt wird der Code kompiliert. Beachten Sie den folgenden Code, um zu sehen, wie sich aus dieser Situation Mehrdeutigkeiten ergeben:
Y<int>::my_type(123);
Diese Code-Anweisung ist vollkommen gültig und weist C ++ an, den Funktionsaufruf an auszuführen Y<int>::my_type
. Wenn my_type
es sich jedoch nicht um eine Funktion, sondern um einen Typ handelt, ist diese Anweisung weiterhin gültig und führt eine spezielle Umwandlung (die Umwandlung im Funktionsstil) durch, bei der es sich häufig um einen Konstruktoraufruf handelt. Der Compiler kann nicht sagen, was wir meinen, also müssen wir hier klarstellen.
Sowohl Java als auch C # führten Generika nach ihrer ersten Sprachveröffentlichung ein. Es gibt jedoch Unterschiede darin, wie sich die Kernbibliotheken bei der Einführung von Generika geändert haben. Die Generika von C # sind nicht nur Compilermagie und konnten daher nicht generiert werden vorhandene Bibliotheksklassen , ohne die Abwärtskompatibilität .
Zum Beispiel in Java die bestehenden Collections Framework wurde vollständig genericised . Java verfügt nicht über eine generische und eine nicht generische Legacy-Version der Sammlungsklassen. In mancher Hinsicht ist dies viel sauberer - wenn Sie eine Sammlung in C # verwenden müssen, gibt es kaum einen Grund, sich für die nicht generische Version zu entscheiden, aber diese Legacy-Klassen bleiben bestehen und überladen die Landschaft.
Ein weiterer bemerkenswerter Unterschied sind die Enum-Klassen in Java und C #. Javas Enum hat diese etwas gewundene Definition:
// java.lang.Enum Definition in Java
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
(Siehe Angelika Langers sehr klare Erklärung, warum dies so ist. Im Wesentlichen bedeutet dies, dass Java typsicheren Zugriff von einer Zeichenfolge auf seinen Enum-Wert gewähren kann:
// Parsing String to Enum in Java
Colour colour = Colour.valueOf("RED");
Vergleichen Sie dies mit der Version von C #:
// Parsing String to Enum in C#
Colour colour = (Colour)Enum.Parse(typeof(Colour), "RED");
Da Enum bereits in C # vorhanden war, bevor Generika in die Sprache eingeführt wurden, konnte die Definition nicht geändert werden, ohne den vorhandenen Code zu beschädigen. Wie Sammlungen verbleibt es in diesem Kernzustand in den Kernbibliotheken.
ArrayList
um List<T>
es in einen neuen Namensraum zu setzen. Tatsache ist, dass im Quellcode eine Klasse angezeigt wird, da ArrayList<T>
diese im IL-Code zu einem anderen vom Compiler generierten Klassennamen wird, sodass keine Namenskonflikte auftreten können.
11 Monate zu spät, aber ich denke, diese Frage ist bereit für einige Java Wildcard-Sachen.
Dies ist eine syntaktische Funktion von Java. Angenommen, Sie haben eine Methode:
public <T> void Foo(Collection<T> thing)
Angenommen, Sie müssen sich nicht auf den Typ T im Methodenkörper beziehen. Sie deklarieren einen Namen T und verwenden ihn dann nur einmal. Warum sollten Sie sich also einen Namen dafür überlegen müssen? 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 nur einmal an dieser Stelle erscheinen muss.
Mit Platzhaltern können Sie nichts tun, was Sie auch nicht mit einem benannten Typparameter tun können (so werden diese Dinge immer in C ++ und C # ausgeführt).
class Foo<T extends List<?>>
und verwenden, Foo<StringList>
aber in C # müssen Sie diesen zusätzlichen Typparameter hinzufügen: class Foo<T, T2> where T : IList<T2>
und den klobigen verwenden Foo<StringList, String>
.
Wikipedia bietet großartige Beschreibungen, in denen sowohl Java / C # -Generiken als auch Java-Generika / C ++ - Vorlagen verglichen werden . Der Hauptartikel über Generika scheint etwas überladen zu sein, enthält aber einige gute Informationen.
Die größte Beschwerde ist das Löschen von Typen. Generika werden zur Laufzeit nicht erzwungen. Hier ist ein Link zu einigen Sun-Dokumenten zu diesem Thema .
Generika werden durch Typlöschung implementiert: Generische Typinformationen sind nur zur Kompilierungszeit vorhanden, danach werden sie vom Compiler gelöscht.
C ++ - Vorlagen sind tatsächlich viel leistungsfähiger als ihre C # - und Java-Gegenstücke, da sie zur Kompilierungszeit ausgewertet werden und die Spezialisierung unterstützen. Dies ermöglicht die Template-Meta-Programmierung und macht den C ++ - Compiler gleichbedeutend mit einer Turing-Maschine (dh während des Kompilierungsprozesses können Sie alles berechnen, was mit einer Turing-Maschine berechenbar ist).
In Java sind Generika nur auf Compilerebene verfügbar, sodass Sie Folgendes erhalten:
a = new ArrayList<String>()
a.getClass() => ArrayList
Beachten Sie, dass der Typ von 'a' eine Array-Liste ist, keine Liste von Zeichenfolgen. Die Art einer Liste von Bananen würde also einer Liste von Affen entsprechen.
Sozusagen.
Es sieht so aus, als ob es neben anderen sehr interessanten Vorschlägen einen gibt, bei dem es darum geht, Generika zu verfeinern und die Abwärtskompatibilität zu brechen:
Derzeit werden Generika mithilfe des Löschvorgangs implementiert. Dies bedeutet, dass die generischen Typinformationen zur Laufzeit nicht verfügbar sind, was das Schreiben von Code erschwert. Auf diese Weise wurden Generika implementiert, um die Abwärtskompatibilität mit älterem nicht generischem Code zu unterstützen. Reified Generics würden die generischen Typinformationen zur Laufzeit verfügbar machen, wodurch nicht generischer Legacy-Code beschädigt würde. Neal Gafter hat jedoch vorgeschlagen, Typen nur dann reifizierbar zu machen, wenn dies angegeben ist, um die Abwärtskompatibilität nicht zu beeinträchtigen.
NB: Ich habe nicht genug Punkte, um Kommentare abzugeben. Sie können diese also als Kommentar zu einer geeigneten Antwort verschieben.
Entgegen der landläufigen Meinung, von der ich nie verstehe, woher sie stammt, hat .net echte Generika implementiert, ohne die Abwärtskompatibilität zu beeinträchtigen, und sie haben sich explizit darum bemüht. Sie müssen Ihren nicht generischen .net 1.0-Code nicht in generische Codes ändern, um in .net 2.0 verwendet zu werden. Sowohl die generischen als auch die nicht generischen Listen sind in .Net Framework 2.0 auch bis 4.0 noch verfügbar, genau 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ärtscode-Kompatibilität wird seit 1.0 bis jetzt immer beibehalten ... Selbst in .net 4.0 müssen Sie die Option verwenden, eine nicht generische Klasse ab 1.0 BCL zu verwenden, wenn Sie dies möchten.
Ich denke also nicht, dass Java die Abwärtskompatibilität brechen muss, um echte Generika zu unterstützen.
ArrayList<Foo>
, die es an eine ältere Methode übergeben möchte, die eine ArrayList
mit Instanzen von füllen soll Foo
. Wenn ein ArrayList<foo>
nicht ist ArrayList
, wie bringt man das zum Laufen ?