Warum ist das in Ordnung und wird meistens erwartet:
abstract type Shape
{
abstract number Area();
}
concrete type Triangle : Shape
{
concrete number Area()
{
//...
}
}
... obwohl das nicht in Ordnung ist und sich niemand beschwert:
concrete type Name : string
{
}
concrete type Index : int
{
}
concrete type Quantity : int
{
}
Meine Motivation ist es, die Verwendung des Typsystems für die Überprüfung der Kompilierzeit auf Korrektheit zu maximieren.
PS: Ja, ich habe diese und Verpackung ist ein Hacky Work-around.
Antworten:
Ich nehme an, Sie denken an Sprachen wie Java und C #?
In diesen Sprachen sind Primitive (wie
int
) im Grunde genommen ein Kompromiss für die Leistung. Sie unterstützen nicht alle Funktionen von Objekten, sind jedoch schneller und mit weniger Aufwand verbunden.Damit Objekte die Vererbung unterstützen, muss jede Instanz zur Laufzeit "wissen", zu welcher Klasse sie gehört. Andernfalls können überschriebene Methoden zur Laufzeit nicht aufgelöst werden. Für Objekte bedeutet dies, dass Instanzdaten zusammen mit einem Zeiger auf das Klassenobjekt im Speicher gespeichert werden. Wenn solche Informationen auch zusammen mit primitiven Werten gespeichert werden sollen, steigt der Speicherbedarf sprunghaft an. Ein 16-Bit-Integer-Wert würde seine 16-Bit-Werte und zusätzlich 32- oder 64-Bit-Speicher für einen Zeiger auf seine Klasse benötigen.
Abgesehen von dem Speicheraufwand würden Sie auch erwarten, allgemeine Operationen für Grundelemente wie arithmetische Operatoren außer Kraft setzen zu können. Ohne Subtypisierung können Operatoren wie
+
bis zu einer einfachen Maschinencodeanweisung kompiliert werden. Wenn es überschrieben werden könnte, müssten Sie Methoden zur Laufzeit auflösen, was eine viel kostspieligere Operation ist. (Möglicherweise wissen Sie, dass C # das Überladen von Operatoren unterstützt - dies ist jedoch nicht dasselbe. Das Überladen von Operatoren wird zur Kompilierungszeit behoben, sodass keine Standardlaufzeitstrafe auftritt.)Strings sind keine Primitive, aber sie sind immer noch "speziell" in der Art, wie sie im Gedächtnis dargestellt werden. Zum Beispiel sind sie "interniert", was bedeutet, dass zwei Zeichenkettenliterale, die gleich sind, auf dieselbe Referenz optimiert werden können. Dies wäre nicht möglich (oder zumindest viel weniger effektiv), wenn String-Instanzen auch die Klasse verfolgen sollten.
Was Sie beschreiben, wäre sicherlich nützlich, aber die Unterstützung würde einen Leistungsaufwand für jede Verwendung von Primitiven und Zeichenfolgen erfordern, selbst wenn sie die Vererbung nicht ausnutzen.
Die Sprache Smalltalk erlaubt (glaube ich) die Unterklassifizierung von ganzen Zahlen. Bei der Entwicklung von Java galt Smalltalk jedoch als zu langsam, und der Aufwand, alles als Objekt zu definieren, wurde als einer der Hauptgründe angesehen. Java hat einige Eleganz und konzeptionelle Reinheit geopfert, um eine bessere Leistung zu erzielen.
quelle
string
ist versiegelt, weil es so konzipiert ist, dass es sich unveränderlich verhält. Wenn man von einem String erben könnte, wäre es möglich, veränderbare Strings zu erstellen, was ihn wirklich fehleranfällig machen würde. Tonnenweise Code, einschließlich des .NET-Frameworks selbst, basiert auf Zeichenfolgen, die keine Nebenwirkungen haben. Siehe auch hier, sagt Ihnen das gleiche: quora.com/Why-String-class-in-C-is-a-sealed-classString
der auchfinal
in Java markiert ist .string
in C♯ nur Sand? Nein, natürlich nicht, einstring
in C♯ ist das, was die C♯-Spezifikation sagt. Wie es implementiert wird, ist unerheblich. Eine native Implementierung von C♯ würde Strings als Byte-Arrays implementieren, eine ECMAScript-Implementierung würde sie auf ECMAScript-String
s abbilden usw.Was eine Sprache vorschlägt, ist keine Unterklasse, sondern eine Untertypisierung . Mit Ada können Sie beispielsweise abgeleitete Typen oder Untertypen erstellen . Der Abschnitt Ada Programming / Type System ist lesenswert, um alle Details zu verstehen. Sie können den Wertebereich einschränken, was Sie die meiste Zeit wünschen:
Sie können beide Typen als Ganzzahlen verwenden, wenn Sie sie explizit konvertieren. Beachten Sie auch, dass Sie keinen anstelle eines anderen verwenden können, selbst wenn die Bereiche strukturell gleich sind (Typen werden durch Namen überprüft).
Die oben genannten Typen sind inkompatibel, obwohl sie denselben Wertebereich darstellen.
(Aber Sie können Unchecked_Conversion verwenden. Sagen Sie den Leuten nicht, dass ich Ihnen das gesagt habe.)
quelle
type Index is -MAXINT..MAXINT;
was irgendwie nichts für mich tut, da alle ganzen Zahlen gültig wären? Welche Art von Fehler würde ich erhalten, wenn ein Winkel an einen Index übergeben würde, wenn nur die Bereiche überprüft würden?Ich denke, das könnte eine X / Y-Frage sein. Wichtige Punkte aus der Frage ...
... und aus deinem Kommentar heraus :
Entschuldigen Sie, wenn mir etwas fehlt, aber ... Wenn dies Ihre Ziele sind, warum um alles in der Welt sprechen Sie dann von Vererbung? Implizite Substituierbarkeit ist ... wie ... die ganze Sache. Weißt du, das Liskov-Substitutionsprinzip?
Was Sie in Wirklichkeit zu wollen scheinen, ist das Konzept eines "starken Typedef" - wobei "etwas" z. B. ein
int
in Bezug auf Reichweite und Repräsentation, aber nicht in Kontexte zu ersetzen ist, die ein erwartenint
und umgekehrt. Ich würde vorschlagen, nach Informationen zu diesem Begriff zu suchen, und wie auch immer Ihre gewählte (n) Sprache (n) ihn nennen. Auch hier ist es buchstäblich das Gegenteil von Vererbung.Und für diejenigen, die eine X / Y-Antwort nicht mögen, denke ich, dass der Titel immer noch mit Bezug auf den LSP beantwortbar ist. Primitive Typen sind primitiv, weil sie etwas sehr Einfaches tun, und das ist alles, was sie tun . Ihre Vererbung und damit die Unendlichkeit ihrer möglichen Auswirkungen zuzulassen, würde bestenfalls zu einer großen Überraschung und im schlimmsten Fall zu einer tödlichen LSP-Verletzung führen. Wenn ich optimistisch annehmen darf, dass es Thales Pereira nichts ausmacht, diesen phänomenalen Kommentar zu zitieren :
Wenn jemand einen primitiven Typus in einer vernünftigen Sprache sieht, geht er zu Recht davon aus, dass er immer nur sein einziges kleines Ding tut, sehr gut und ohne Überraschungen. Primitive Typen verfügen nicht über Klassendeklarationen, die angeben, ob sie vererbt werden oder nicht, und deren Methoden überschrieben werden. Wenn dies der Fall wäre, wäre dies in der Tat sehr überraschend (und würde die Abwärtskompatibilität völlig aufheben, aber mir ist bewusst, dass dies eine Abwärtsantwort auf die Frage ist, warum X nicht mit Y entworfen wurde).
... obwohl, wie Mooing Duck in seiner Antwort betonte , Sprachen, die das Überladen von Operatoren ermöglichen, es dem Benutzer ermöglichen, sich in ähnlichem oder gleichem Ausmaß zu verwirren, wenn er es wirklich will, und es daher zweifelhaft ist, ob dieses letzte Argument zutrifft. Und ich werde jetzt aufhören, die Kommentare anderer Leute zusammenzufassen, heh.
quelle
Um eine Vererbung mit virtuellem Versand zu ermöglichen (was im Anwendungsdesign oft als wünschenswert angesehen wird), benötigt man Laufzeit-Typinformationen. Für jedes Objekt müssen einige Daten bezüglich des Objekttyps gespeichert werden. Einem primitiven Element fehlen per Definition diese Informationen.
Es gibt zwei (verwaltete, auf einer VM ausgeführte) OOP-Hauptsprachen mit Grundelementen: C # und Java. Viele andere Sprachen haben in erster Linie keine Grundelemente oder verwenden ähnliche Gründe, um sie zuzulassen / zu verwenden.
Primitive sind ein Kompromiss für die Leistung. Für jedes Objekt benötigen Sie Speicherplatz für seinen Objektheader (in Java normalerweise 2 * 8 Byte auf 64-Bit-VMs) sowie seine Felder und eventuelles Auffüllen (in Hotspot belegt jedes Objekt eine Anzahl von Bytes, die einem Vielfachen von entspricht 8). Ein
int
as-Objekt würde also mindestens 24 Byte Speicher benötigen, anstatt nur 4 Byte (in Java).So wurden primitive Typen hinzugefügt, um die Leistung zu verbessern. Sie erleichtern eine ganze Reihe von Dingen. Was bedeutet
a + b
, wenn beide Subtypen sindint
? Irgendeine Art von Verschiebung muss hinzugefügt werden, um die richtige Addition zu wählen. Dies bedeutet virtuellen Versand. Die Möglichkeit, einen sehr einfachen Opcode für das Hinzufügen zu verwenden, ist sehr viel schneller und ermöglicht Optimierungen bei der Kompilierung.String
ist ein anderer Fall. Sowohl in Java als auch in C #String
ist ein Objekt. Aber in C # ist es versiegelt, und in Java ist es endgültig. Dies liegt daran, dass sowohl in der Java- als auch in der C # -StandardbibliothekString
s unveränderlich sein müssen und eine Unterklasse diese Unveränderlichkeit aufheben würde.Im Falle von Java kann (und tut) die VM Strings internieren und "bündeln", um eine bessere Leistung zu erzielen. Dies funktioniert nur, wenn Strings wirklich unveränderlich sind.
Außerdem muss man primitive Typen selten in Unterklassen einteilen. Solange Primitive nicht in Unterklassen eingeteilt werden können, gibt es eine Menge netter Dinge, die uns die Mathematik über sie erzählt. Zum Beispiel können wir sicher sein, dass Addition kommutativ und assoziativ ist. Das sagt uns die mathematische Definition von ganzen Zahlen. Darüber hinaus können wir Invarianten in vielen Fällen einfach über Schleifen durch Induktion beweisen. Wenn wir Unterklassen zulassen
int
, verlieren wir die Werkzeuge, die uns die Mathematik zur Verfügung stellt, da wir nicht mehr garantieren können, dass bestimmte Eigenschaften erhalten bleiben. Daher würde ich sagen, dass die Fähigkeit , primitive Typen nicht zu unterordnen, eigentlich eine gute Sache ist. Weniger Dinge, die jemand kaputt machen kann, und ein Compiler kann oft beweisen, dass er bestimmte Optimierungen vornehmen darf.quelle
to allow inheritance, one needs runtime type information.
Falsch.For every object, some data regarding the type of the object has to be stored.
Falsch.There are two mainstream OOP languages that feature primitives: C# and Java.
Was, ist C ++ jetzt kein Mainstream? Ich würde es verwenden , wie meine Widerlegung als Laufzeittypinformationen ist ein C ++ Begriff. Es ist absolut nicht erforderlich, es sei denn mitdynamic_cast
odertypeid
. Und selbst wenn RTTI aktiviert ist, belegt die Vererbung nur dann Speicherplatz, wenn eine Klasse übervirtual
Methoden verfügt , auf die eine klassenbezogene Methodentabelle pro Instanzdynamic_cast
undtypeinfo
verlangen das. Der virtuelle Versand wird praktisch mithilfe eines Zeigers auf die vtable für die konkrete Klasse des Objekts implementiert, sodass die richtigen Funktionen aufgerufen werden können, erfordert jedoch nicht die in RTTI enthaltenen Details zu Typ und Beziehung. Der Compiler muss lediglich wissen, ob die Klasse eines Objekts polymorph ist und wenn ja, wie der vptr der Instanz lautet. Mit können virtuell versandte Klassen trivial zusammengestellt werden-fno-rtti
.dynamic_cast
Klassen ohne virtuellen Versand. Der Grund für die Implementierung ist, dass RTTI im Allgemeinen als verborgenes Mitglied einer vtable implementiert wird.In den gängigen starken statischen OOP-Sprachen wird die Untertypisierung in erster Linie als eine Möglichkeit angesehen, einen Typ zu erweitern und die aktuellen Methoden des Typs zu überschreiben.
Zu diesem Zweck enthalten 'Objekte' einen Zeiger auf ihren Typ. Dies ist ein Overhead: Der Code in einer Methode, die eine
Shape
Instanz verwendet, muss zuerst auf die Typinformationen dieser Instanz zugreifen, bevor er die richtigeArea()
aufzurufende Methode kennt .Ein Grundelement lässt in der Regel nur Operationen zu, die in Anweisungen in einer Maschinensprache übersetzt werden können, und enthält keine Typinformationen. Eine Ganzzahl langsamer zu machen, damit jemand sie unterordnen konnte, war unattraktiv genug, um zu verhindern, dass Sprachen, die dies taten, zum Mainstream wurden.
Also die Antwort auf:
Ist:
Wir fangen jedoch an, Sprachen zu entwickeln, die statische Überprüfungen auf der Grundlage von Eigenschaften anderer Variablen als 'type' ermöglichen, z. B. F # hat "dimension" und "unit", sodass Sie einem Bereich beispielsweise keine Länge hinzufügen können .
Es gibt auch Sprachen, die 'benutzerdefinierte Typen' zulassen, die die Funktion eines Typs nicht ändern (oder austauschen), sondern nur bei der statischen Typprüfung helfen. Siehe die Antwort von coredump.
quelle
Ich bin mir nicht sicher, ob ich hier etwas übersehen habe, aber die Antwort ist ziemlich einfach:
Beachten Sie, dass es wirklich nur zwei starke statische OOP - Sprachen sind , die selbst haben Primitiven, AFAIK: Java und C ++. (Tatsächlich bin ich mir bei letzterem nicht sicher, ich weiß nicht viel über C ++ und was ich bei der Suche gefunden habe, war verwirrend.)
In C ++ sind Primitive im Grunde genommen ein Erbe (Wortspiel beabsichtigt) von C. Sie nehmen also nicht am Objektsystem (und damit an der Vererbung) teil, da C weder ein Objektsystem noch eine Vererbung hat.
In Java sind Grundelemente das Ergebnis eines fehlgeleiteten Versuchs, die Leistung zu verbessern. Primitive sind auch die einzigen Werttypen im System. Tatsächlich ist es unmöglich, Werttypen in Java zu schreiben, und es ist unmöglich, dass Objekte Werttypen sind. Abgesehen von der Tatsache, dass Primitive nicht am Objektsystem teilnehmen und daher die Idee der "Vererbung" nicht einmal Sinn macht, selbst wenn Sie von ihnen erben könnten, wären Sie nicht in der Lage, die " Vererbung" aufrechtzuerhalten. Wertigkeit ". Dies unterscheidet sich von zB C♯ , die tut Werttypen haben (
struct
s), die dennoch Objekte sind.Eine andere Sache ist, dass das Nicht-Erben auch nicht nur für Primitive gilt. In C♯ können
struct
s implizit s erbenSystem.Object
und implementiereninterface
, sie können jedoch weder von es noch von s erbenclass
oder erbenstruct
. Auchsealed
class
kann es nicht vererbt werden. In Java kannfinal
class
es nicht von geerbt werden.tl; dr :
final
odersealed
in Java oder C♯,struct
s in C♯,case class
es in Scala)quelle
virtual
, was bedeutet, dass sie nicht LSP gehorchen. ZBstd::string
ist kein Primitiv, aber es verhält sich sehr viel wie nur ein anderer Wert. Solche Wertesemantiken sind weit verbreitet, der gesamte STL-Teil von C ++ geht davon aus.int
verwendeten Speicherbereich Speicher zuweisen . Jede Zuordnung dauert in der Größenordnung von 100 ns zuzüglich des Overheads der Speicherbereinigung. Vergleichen Sie dies mit dem einzelnen CPU-Zyklus, der durch Hinzufügen von zwei Grundelementen belegt wirdint
. Ihre Java-Codes würden mitkriechen, wenn die Designer der Sprache etwas anderes entschieden hätten.int
, führen sie also genau dasselbe aus. (Scala-native kompiliert sie in primitive Maschinenregister, Scala.js kompiliert sie in primitive ECMAScript-DateienNumber
.) Ruby hat keine Primitive, aber YARV und Rubinius kompilieren Ganzzahlen in primitive Maschinen-Ganzzahlen, JRuby kompiliert sie in JVM-Primitivelong
. Nahezu jede Lisp-, Smalltalk- oder Ruby-Implementierung verwendet Primitive in der VM . Hier werden Leistungsoptimierungen durchgeführt…Joshua Bloch in „Effective Java“ empfiehlt, das Design explizit zur Vererbung oder zum Verbot zu verwenden. Primitive Klassen sind nicht für die Vererbung vorgesehen, da sie unveränderlich sind und die Ermöglichung der Vererbung das in Unterklassen ändern kann, wodurch das Liskov-Prinzip verletzt wird und viele Fehler auftreten können.
Wie auch immer, warum ist das eine hackige Umgehung? Sie sollten die Komposition wirklich der Vererbung vorziehen. Wenn der Grund die Leistung ist, haben Sie einen Punkt und die Antwort auf Ihre Frage ist, dass es nicht möglich ist, alle Features in Java zu platzieren, da es einige Zeit dauert, alle verschiedenen Aspekte des Hinzufügens eines Features zu analysieren. Java hatte zum Beispiel keine Generics vor 1.5.
Wenn Sie viel Geduld haben, haben Sie Glück, denn es gibt einen Plan , Java um Wertklassen zu erweitern, mit denen Sie Ihre Wertklassen erstellen können, um die Leistung zu steigern und gleichzeitig mehr Flexibilität zu erhalten.
quelle
Auf der abstrakten Ebene können Sie alles, was Sie wollen, in eine Sprache einfügen, die Sie entwerfen.
Auf der Implementierungsebene ist es unvermeidlich, dass einige dieser Dinge einfacher zu implementieren sind, einige kompliziert sind, einige schneller gemacht werden können, andere langsamer sein müssen und so weiter. Um dies zu berücksichtigen, müssen Designer häufig schwierige Entscheidungen treffen und Kompromisse eingehen.
Auf der Implementierungsebene besteht eine der schnellsten Möglichkeiten für den Zugriff auf eine Variable darin, ihre Adresse herauszufinden und den Inhalt dieser Adresse zu laden. In den meisten CPUs gibt es spezielle Anweisungen zum Laden von Daten von Adressen, und diese Anweisungen müssen normalerweise wissen, wie viele Bytes sie laden müssen (eins, zwei, vier, acht usw.) und wo sie die Daten ablegen müssen, die sie laden (einzelnes Register, Register) Paar, erweitertes Register, anderer Speicher usw.). Wenn der Compiler die Größe einer Variablen kennt, kann er genau wissen, welche Anweisung für die Verwendung dieser Variablen ausgegeben werden soll. Wenn der Compiler die Größe einer Variablen nicht kennt, müsste er auf etwas Komplizierteres und wahrscheinlich Langsameres zurückgreifen.
Auf der abstrakten Ebene besteht der Punkt der Untertypisierung darin, Instanzen eines Typs zu verwenden, bei denen ein gleicher oder allgemeinerer Typ erwartet wird. Mit anderen Worten, Code kann geschrieben werden, der ein Objekt eines bestimmten Typs oder etwas anderes erwartet, ohne vorher zu wissen, was genau dies sein würde. Und da mehr abgeleitete Typen mehr Datenelemente hinzufügen können, muss ein abgeleiteter Typ nicht unbedingt die gleichen Speicheranforderungen wie seine Basistypen haben.
Auf der Implementierungsebene gibt es keine einfache Möglichkeit für eine Variable einer vorgegebenen Größe, eine Instanz unbekannter Größe zu speichern und auf eine Weise zuzugreifen, die Sie normalerweise als effizient bezeichnen würden. Es gibt jedoch eine Möglichkeit, Dinge ein wenig zu verschieben und eine Variable zu verwenden, um das Objekt nicht zu speichern, sondern um das Objekt zu identifizieren und es an einem anderen Ort speichern zu lassen. Auf diese Weise entsteht eine Referenz (z. B. eine Speicheradresse) - eine zusätzliche Indirektionsebene, die sicherstellt, dass eine Variable nur Informationen mit fester Größe enthalten muss, solange wir das Objekt über diese Informationen finden können. Um dies zu erreichen, müssen wir nur die Adresse (feste Größe) laden und können dann wie gewohnt mit den Offsets des Objekts arbeiten, von denen wir wissen, dass sie gültig sind, auch wenn das Objekt mehr Daten in Offsets hat, die wir nicht kennen. Wir können das tun, weil wir nicht
Auf der abstrakten Ebene können Sie mit dieser Methode einen (Verweis auf einen)
string
in einerobject
Variablen speichern , ohne die Informationen zu verlieren, die ihn zu einem machenstring
. Es ist in Ordnung für alle Typen, so zu arbeiten, und man könnte auch sagen, dass es in vielerlei Hinsicht elegant ist.Auf der Implementierungsebene erfordert die zusätzliche Indirektionsebene jedoch mehr Anweisungen, und auf den meisten Architekturen wird jeder Zugriff auf das Objekt etwas langsamer. Sie können zulassen, dass der Compiler mehr Leistung aus einem Programm herausholt, wenn Sie einige häufig verwendete Typen in Ihre Sprache aufnehmen, die nicht über diese zusätzliche Indirektionsebene (die Referenz) verfügen. Durch Entfernen dieser Indirektionsebene kann der Compiler jedoch keine speichersichere Untertypisierung mehr zulassen. Dies liegt daran, dass, wenn Sie Ihrem Typ weitere Datenelemente hinzufügen und einem allgemeineren Typ zuweisen, alle zusätzlichen Datenelemente, die nicht in den für die Zielvariable zugewiesenen Speicherplatz passen, entfernt werden.
quelle
Im Allgemeinen
Wenn eine Klasse abstrakt ist (Metapher: eine Box mit Löchern), ist es in Ordnung (sogar erforderlich, dass etwas verwendbar ist!), Die Löcher zu füllen. Deshalb werden abstrakte Klassen in Unterklassen unterteilt.
Wenn eine Klasse konkret ist (Metapher: ein Kasten voll), kann das Vorhandene nicht geändert werden, da es voll ist, wenn es voll ist. Wir haben keinen Platz mehr, um etwas mehr in die Box aufzunehmen, deshalb sollten wir keine konkreten Klassen unterordnen.
Mit Primitiven
Primitive sind von Entwurf her konkrete Klassen. Sie stellen etwas dar, das bekannt und eindeutig ist (ich habe noch nie einen primitiven Typ mit etwas Abstraktem gesehen, ansonsten ist es kein Primitiv mehr) und das im System weit verbreitet ist. Wenn Sie es zulassen, einen primitiven Typ in Unterklassen einzustufen und Ihre eigene Implementierung für andere bereitzustellen, die sich auf das entworfene Verhalten von Primitiven verlassen, können viele Nebenwirkungen und große Schäden auftreten!
quelle
Normalerweise entspricht die Vererbung nicht der von Ihnen gewünschten Semantik, da Sie Ihren speziellen Typ nicht überall ersetzen können, wo ein Grundelement erwartet wird. Aus Ihrem Beispiel auszuleihen,
Quantity + Index
macht semantisch keinen Sinn, daher ist eine Vererbungsbeziehung die falsche Beziehung.Einige Sprachen haben jedoch das Konzept eines Wertetyps , der die Art der Beziehung ausdrückt, die Sie beschreiben. Scala ist ein Beispiel. Ein Wertetyp verwendet ein Grundelement als zugrunde liegende Darstellung, weist jedoch eine andere Klassenidentität und Operationen auf der Außenseite auf. Das hat den Effekt, einen primitiven Typ zu erweitern, ist aber eher eine Komposition als eine Vererbungsbeziehung.
quelle