Der Zwiespalt: low or high
-
Shade Of Mine schrieb:
Optimizer schrieb:
So etwas wäre Selbstmord.
Warum? Fliegt doch bloss eine BadCastException oder wie sie auch immer heissen mag.
Genau. Compile-Zeit-Fehler grundlos auf Laufzeitfehler verlagern, das ham ma gern.
Es kann dir ja niemand garantieren, dass T Compareable ist.
Wozu auch? Wenn jemand sort() verwenden will, muss T Comparable sein. Ist T das nicht, dann geht sort() nicht und man hat ohne auszukommen.
Ja aber das ist doch genau der Blödsinn. Wofür hab ich dann constraints?
Wie würdest du mein imiginäres Problem mit Plugin und List lösen? "Ich meine, nicht jede Klasse die ich handle muss einen Operator< implementieren. zB Plugin Klassen. Ich kann zwar sagen: a!=b - das ist nicht schwer, aber welches Plugin ist kleiner? Unmöglich zu sagen. Aber dennoch möchte ich 10 Plugins in eine Liste stecken."
Das ist doch kein Problem. Ein UniqueList müsste beispielsweise die Elemente auf Gleichheit prüfen. Wenn man jetzt mal außer Acht lässt, dass Object equals() mitbringt, müsste ich halt eine Interface ImplementsEquals verlangen für meine UniqueList. Ich weiß, dir kommt das umständlich und unschön vor.
Aber es ist nicht wirklich ein Problem, vor allem natürlich bei equals() jetzt nicht.Das will ich ja. Ich will ja eben die Beschränkung umgehen.
Wozu, wenn du die Beschränkung selber definieren kannst? Das ist wie, wenn ich ne Variable final mach und sie dann doch h4x0r. Dann mach ich sie halt nicht final.
Warum?
Ich würde es machen.Dann würde ich deine Liste nicht kaufen.
Der Compiler warnt ja auch nicht zum Spaß. Der Punkt ist, dass die generische Klasse für sich alleine steht. Sie wird nicht zur Kompilierzeit instanziert. Sie steht alleine und weiß über T nur das, was du sagst. Deshalb sind casts sinnlos.
Warum? Ich will ja eben _nicht_ alle Anforderungen festlegen - weil ich optionale Komponenten habe.
Dann extende oder spezialisiere die Liste entsprechend. Wo ist denn das Problem?
Und was machen wir, wenn ich für .sort() IComparable brauche und für .clone() eine .clone() Methode? Habe ich dann
List, SortableListe, CloneableList, SortableCloneableList?Kommt in der Praxis so nicht vor. Guckst du ins Java API, da findest du Lists, Sets und Maps und allerlei Spezialisierungen davon. Wenn du jetzt ne List sortierst, gibt es in der Klasse Collections dafür ne generische Methode.
Der übergibst du entweder nen Comparer<T> oder du rufst die ohne Comparer auf und implementierst dafür Compareable. Ein Set fordert halt, dass du equals() redefiniert hast (oder du übergibst wieder nen Comparer<T>). Glaub mir, es ist schon zu benutzen.
Ich verstehe die Grundsatzfrage zwar, aber das kommt so nicht vor, wie du es schilderst.Ja. Es kann mit Constraints etwas umständlicher sein. Ist mir auch einmal schon passiert. Constraints haben nicht nur Vorteile. Vollkommen klar. Ich will auch nur sagen, dass es ein anderes Konzept ist.
Anders als was? Als in C++? Ne, das sehe ich nicht.
Ich schon. Ich finde auch den Link von kingruedi recht aufschlussreich in der Hinsicht.
Ja, das ist ein Problem. Denn wie mache ich eine SortableClonableList?
class SortableCloneableList<T extends Compareable, Cloneable>
Natürlich ist 'nachschauen' schöner als 'ausprobieren' aber das Resultat ist das selbe. (Anders als bei der Implementierung von Generics in Java - die eben das selbe Resultat liefern wie wenn der User selber castet -> im Gegensatz zu C# wo das Resultat etwas anderes ist).
Nein, bitte sieh es doch ein, dass es C# und Java gleich ist. Das muss dich gar nicht interessieren, dass intern gecastet wird. Der cast geht nie schief, weil der Compiler es schon prüft. Er "schaut" nach, ob dein new Integer() Cloneable und Compareable implementiert und reagiert entsprechend. Der Cast im Bytecode ist vollkommen bedeutungslos und existiert nur aus Kompatibilitätsgründen.
ein, leider nicht so einfach.
Denn eine Klasse muss das Interface implementieren. Man kann also nicht nachträglich ein Interface einfügen. Somit kann ich nicht ein eigenes Interface für einen Container schreiben - weil ich somit alle Objekte ändern müsste die ich je in den Container reingeben will. Das ist etwas doof.Das ist halt ein strengeres System. Der Gedanke ist interessant, aber das hat mir noch keinen Ärger bereitet.
Das hat nichts mit Interfaces zu tun, sondern mit impliziter Typumwandlung. Einfach alle Ctors explicit machen und das Problem ist größtenteils behoben. Allerdings sehe ich den Zusammenhang mit impliziter Typumwandlung hier nicht...
Das sollte ein Beispiel sein. Bei Java gehts nicht, wenn ich Cloneable nicht implementiere. Bei C++ könnte es "zufällig" gehen, z.B. wegen impliziter Typumwandlung. Und das Template checkts nicht und generiert Code, den ich nicht wollte.
-
Constraints an Typparameter sind sogar ein ziemlich neues, eigenes Konzept.
Aha, wann gabs sowas denn des erste mal? Irgendwann in den 80ern. Sooo neu ist das auch wieder nicht.
Kannst du das genauer ausführen? In C# muss man Constraints definieren, sonst kann man nur die System.Object Schnittstelle benutzen!
Das muss man in C++ wohl keineswegs so machen. Ich kann eine sortierte Liste schreiben, und die T-Objekte mit '<' vergleichen, wie ich lustig bin.
Wenns dann doch nicht geht, merkt der Compiler das irgendwann. In C# ist dagegen gleich klar: Wenn ich jetzt ne SortedList<T : ICompareable> benutzen will, muss meine Klasse dieses Interface implementieren.Beide Lösungen sind beschissen.
Java/C#-artige Interfaces an sich sind schonmal richtiger Schrott. Wenn, dann will ich was, was mir Freiheiten einräumt und nicht verkappte ABCs (Abstrakte Basisklassen). Und dass ordentliche Interfaces nicht so schwer sein können, zeigen Sprachen, wie Nice mit seinen "abstract interfaces", Heron mit seinen Interfaces und die hoffentlich bald fertig gestellte BIL (Boost Interface Library) für C++ (auch wenn hier Support seitens der Sprache natürlich toll wäre).
Interfaces sind für mich etwas, das auf strukturellem Subtypeing basieren sollte. Es geht um's Konzept, die zur Verfügung stehende Schnittstelle. Was da jetzt geerbt/"implementiert" wurde ist da vollkommen egal. Nominative Vererbung ist an anderer Stelle sinnvoll, aber nicht hier.Naja, ein wenig vom Thema abgekommen.
Stoustrup hat doch einen Proposal zum Thema Constrains veröffentlich. Das Konzept ist vielleicht auch noch nicht perfekt, aber doch deutlich besser, als das, was Java/C# da machen.
Man muss sich doch nurmal "Klassen" (keine OO-Klassen) in Haskell ansehen, um zu sehen, wie man Typen beschränken kann.
-
Helium schrieb:
Constraints an Typparameter sind sogar ein ziemlich neues, eigenes Konzept.
Aha, wann gabs sowas denn des erste mal? Irgendwann in den 80ern. Sooo neu ist das auch wieder nicht.
Danke, dass du mich auch noch einmal darauf hinweist.
Mir fällt gerade auf, dass ich meine Trumpfkarte noch gar nicht ausgespielt habe:void sortiereNachBetragDerZahlen(List<? extends Number> list) { // frisst List<Number>, List<Float>, List<Integer>, ... // bei *voller* *statischer* Typsicherheit kann ich mit der list alles // machen, außer natürlich Objekte hinzufügen... }
Gibt's das auch schon länger? Mit C++ Templates geht das jedenfalls nicht.
Dann gibt's noch das hier:
List<? super Integer> list; list = new List<Integer>(); list = new List<Number>(); ... // Ich kann in die Liste immer ein Integer-Objekt hinzufügen, auslesen aber nicht
Und Bivarianz:
void funktion(List<?> list); // Nimmt alle Listen
@Shade: Ich denke schon, dass solche Möglichkeiten die Bezeichnung "neues Sprachfeature" rechtfertigen und das es mehr ist als nur ein Bytecode-Hack.
-
Optimizer schrieb:
Gibt's das auch schon länger? Mit C++ Templates geht das jedenfalls nicht.
Klar. Einfach ein Concept dafür schreiben. Schliesslich kann man ja recht einfach checken ob eine Klasse von einer anderen abgeleitet ist.
und man kann der Liste dann auch elemente hinzufügen wenn man will
Da man den genauen Typ trotzdem kennt.
Dann gibt's noch das hier:
List<? super Integer> list; list = new List<Integer>(); list = new List<Number>(); ... // Ich kann in die Liste immer ein Integer-Objekt hinzufügen, auslesen aber nicht
Geht so in C++ natürlich nicht, weil List<Int> und List<Num> 2 verschiedene Typen sind. Lässt sich aber ein Wrapper schreiben und dann geht es auch.
Und Bivarianz:
void funktion(List<?> list); // Nimmt alle Listen
??
das ist wohl das elementarste was es gibt...@Shade: Ich denke schon, dass solche Möglichkeiten die Bezeichnung "neues Sprachfeature" rechtfertigen und das es mehr ist als nur ein Bytecode-Hack.
Na dann erklär mir mal die Unterschiede zwischen Java Generics und selber casten. Ich sehe da nur etwas Schreibaufwand und OK: man kann einige Fehler schon zur Compiletime wissen. Aber ist das jetzt so neu??
-
Ich sehe da nur etwas Schreibaufwand und OK: man kann einige Fehler schon zur Compiletime wissen.
lol? Ist das vielleicht gar nichts? Ich kann auf generischen Typen (z.B. Listen) arbeiten. Ich kann (mehrfache) Type-Bounds vorgeben. Ich kann mehrdeutige Typebounds vorgeben. Ich kann generische Typen polymorph behandeln.
Davon können C++ Templates bei weitem nicht alles. Ein Wrapperklasse zu schreiben ist bei praktisch unendlich vielen möglichen abgeleiteten Klassen keine Alternative, vor allem auch, weil du den Wrapper immer wieder anpassen musst, wenn du eine neue Klasse definierst.Und alles bei statischer Typsicherheit. Das ist wohl gar nichts. Wie du immer auf den Bytecode-Cast schaust... der ist nur Zierde. Den braucht man für Generics nicht, nur für die VM.
Seit wann muss ein Sprachfeature neue Elemente zum Bytecode hinzufügen? Selbstverständlich ist das ein Sprachfeature und kein Hack. Wieso willst du das nicht anerkennen?
Du stellst immer eine Verbindung zwischen Generics und casten her, das ist aber falsch! Generics haben mit casten nichts zu tun. Ich kann in C++ natürlich auch so krasse Sachen nutzen. Über void* Pointer geht alles.
Das zählt genausowenig, wie in Java zu casten, das ist nicht das selbe.
-
Optimizer schrieb:
Ich kann (mehrfache) Type-Bounds vorgeben. Ich kann mehrdeutige Typebounds vorgeben. Ich kann generische Typen polymorph behandeln.
Und was davon kann ich in C++ nicht?
OK, die Konzepte sind aufwendiger zu implementieren.
Dafür kann ich ohne Probleme ein C++ Template schreiben welches du nie in Java implementieren kannstJava verlangt halt immer ein Interface - wenn ich das in C++ genauso machen würde, wären die Concept checks auch viel simpler (weil ich nur noch die Vererbungshierachie checken müsste).
Ich sehe das übrigens ähnlich wie Helium (nur nicht ganz so extrem). Dadurch dass ich dauernd ein Interface brauche - und es nicht reicht, dass die Klasse Run() anbietet, sondern auch noch von IRunable erben muss, ist es IMHO ziemlich lästig zu arbeiten. Aber OK - in C++ habe ich eben die möglichkeit IRunable zu implementieren oder Run() ganz unabhängig davon zu implementieren. Diese flexibilität macht die Concept Checks natürlich komplizierter, hat aber den Vorteil, dass ein Interface unabhängig von Interface-Klassen sind.
Ein Wrapperklasse zu schreiben ist bei praktisch unendlich vielen möglichen abgeleiteten Klassen keine Alternative, vor allem auch, weil du den Wrapper immer wieder anpassen musst, wenn du eine neue Klasse definierst.
Nein. Der Wrapper ist selber ein Template - damit muss ich ihn nicht öfters schreiben.
Allerdings reicht 1 Wrapper nicht, da man die List selber nochmal wrappen muss: man braucht also 3 Klassen mit zusammen etwa 30-40 Zeilen. Tut also nicht wirklich weh.Seit wann muss ein Sprachfeature neue Elemente zum Bytecode hinzufügen? Selbstverständlich ist das ein Sprachfeature und kein Hack. Wieso willst du das nicht anerkennen?
OK, dann sage ich eben: Generics in Java nicht nicht konsequent durchgezogen.
Du stellst immer eine Verbindung zwischen Generics und casten her, das ist aber falsch! Generics haben mit casten nichts zu tun. Ich kann in C++ natürlich auch so krasse Sachen nutzen. Über void* Pointer geht alles.
Das zählt genausowenig, wie in Java zu casten, das ist nicht das selbe.Wie arbeiten denn die Generics? Sie machen die casts statt dem User, oder? Man hat keine Typinformationen mehr zur Laufzeit, man muss primitive Typen boxen um sie in templates zu verwenden und das perverseste:
T foo=new T();
geht nicht - weil T zur Laufzeit nicht bekannt ist. Sowas finde ich sehr unintuitive und ist ein klares Tribut dass man dem "Bytecode nicht ändern" zahlt. Insofern hat der Bytecode damit schon etwas zu tun - denn würde man Bytecode-Kompatibilität aufgeben, würden Generics anders aussehen (nämlich so wie in C#)OK, nennen wir es nicht Hack - aber tatsache bleibt, dass die Generics in Java mager sind. C# zeigt, wie man es richtig machen kann. Hier sind Generics Native - was man daran erkennt, dass man Generics zur Runtime genauso behandeln kann wie zur Compiletime. Von der Performance mal abgesehen
-
T foo=new T();
geht nicht - weil T zur Laufzeit nicht bekannt ist.Vermisst du das wirklich so sehr? Ich habe es noch nie gebraucht und ich programmiere jetzt doch schon längere Zeit mit der 2005 Beta.
In diesem Zusammenhang ist übrigens interessant, dass man sich dagegen entschieden hat, noch weitere mögliche constraints als den Standardkonstruktor aufzunehmen.
Wahrscheinlich gibt's das also wirklich nur, damit man einen Standardkonstruktor für Valuetypes in Arrays voraussetzen kann. Ein "Manko", an dem Java nicht leidet, weil die Valuetypes nicht in den Generics nutzbar sind. Rein OO betrachtet ist damit die Java-Lösung (ein Integer anstatt ein int) schöner. Aber natürlich nicht sehr performant.Sowas finde ich sehr unintuitive und ist ein klares Tribut dass man dem "Bytecode nicht ändern" zahlt. Insofern hat der Bytecode damit schon etwas zu tun - denn würde man Bytecode-Kompatibilität aufgeben, würden Generics anders aussehen (nämlich so wie in C#)
Klar. Wir müssen ja keine Sekunde darüber diskutieren, dass Generics nicht so toll implementiert sind.
Darum geht es mir ja auch gar nicht. Aber es hat sich vorhin so angehört, als würdest du von der schlechten Implementierung, die darin besteht, casts einzusetzen, darauf schließen, dass es kein eigenes Sprachfeature ist, sondern nur ein versteckter Cast.
Das ist aber natürlich Unsinn. Dann wären C++ Templates auch nur ein Hack und zwar noch ein viel einfacherer, weil der Compiler nur dumm ausprobiert. Selbstverständlich sind die Templates aber ein tolles und mächtiges Sprachfeature.Offtopic:
Mir gefallen die Generics btw. in C# auch besser. C# ist ein fantastisches Beispiel für "besser gut geklaut als schlecht selber gemacht" - in vielerlei Hinsicht. In Java konnte man so weit aber nicht gehen.
Es sind schon viel zu viele hunderte Millionen Zeilen Quellcode in Java geschrieben worden. C# ist erst dabei, sich zu etablieren.
Ich halte es aber nicht für unmöglich, dass sich das in Java auch mal ändern wird.
Man muss zu guter Letzt auch feststellen, dass sich die Situation durch die Generics keineswegs verschlechtert hat. Das wird zu oft übersehen.
-
OK, kannst du nochmal genau darauf eingehen, wie es mit co- und contra-Varianz in Java aussieht?
Kann ich sagen, dass Foo<T> ein Subtyp von Foo<S> ist, wenn T ein Subtyp von S ist (so wie es bei Arrays ist)?
Wenn ja, was passiert, wenn ich einen covarianten Typ in einer contravarianten position verwende? Also ich kenne das jetzt z.B. von Scala:class Array[+a] { def apply(index: int): a; def update(index: int, elem: a): unit; // ^ covariant typparameter taucht an contravarianter Stelle auf }
Das plus vor dem a bedeutet, dass was ich oben beschrieben habe.
-
Kann ich sagen, dass Foo<T> ein Subtyp von Foo<S> ist, wenn T ein Subtyp von S ist (so wie es bei Arrays ist)?
Nein, so geht es nicht.
Die generischen Typen sind zunächst mal invariant, d.h. es geht nurFoo<T> foo = new Foo<T>();
Weil das ein wenig spröde ist hat man nen bivarianten wildcard eingeführt.
Foo<?> foo = new Foo<IRGENDWAS>();
Dort kann ich aber natürlich nichts machen, wofür man irgendwie Informationen über den parametrisierten Typ braucht.
List<?> foo; foo.size();
geht natürlich.
Deshalb kann man typebounds definieren, z.B. Kovariant:
List<? extends Number> list = new List<Integer>();
zu beachten ist hier, dass
List<Number> list = new List<Integer>();
nicht geht.
Hier kann ich beliebig Number-Objekte aus list auslesen, aber keine hinzufügen. Ich könnte ja sonst ein Integer hinzufügen, obwohl die Liste Doubles will.
Das ist das Problem bei den Arrays von Java und C#. Sie sind kovariant, erlauben aber Schreibzugriff trotzdem - eine böse Lücke im statischen Typsystem.Und dann gibt's noch kontravarianz (mit super), wo man nur reinschreiben, aber nicht auslesen kann.
Wenn ja, was passiert, wenn ich einen covarianten Typ in einer contravarianten position verwende?
Der Compiler weist alles zurück, was nicht 100%ig gut gehen kann. Die einzelnen Einschränkungen stehen oben für die 4 möglichen Fälle.
-
Danke