Warum verhindern die starken statischen OOP-Sprachen des Mainstreams das Erben von Primitiven?

53

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.

Den
quelle
1
Kommentare sind nicht für eine längere Diskussion gedacht. Diese Unterhaltung wurde in den Chat verschoben .
maple_shaft
Ich hatte eine ähnliche Motivation in dieser Frage , vielleicht finden Sie es interessant.
default.kramer
Ich wollte eine Antwort hinzufügen, die die Idee "Sie wollen keine Vererbung" bestätigt, und diese Umhüllung ist sehr leistungsfähig, einschließlich der impliziten oder expliziten Umwandlung (oder von Fehlern), die Sie möchten, insbesondere mit JIT-Optimierungen, die Sie vorschlagen bekomme sowieso fast die gleiche Leistung, aber du hast auf diese Antwort verlinkt :-) Ich würde nur hinzufügen, es wäre schön, wenn Sprachen Features hinzufügen, um den für die Weiterleitung von Eigenschaften / Methoden benötigten Code zu reduzieren, besonders wenn es nur einen einzigen Wert gibt.
Mark Hurd

Antworten:

82

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.

JacquesB
quelle
12
@Den: stringist 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-class
Doc Brown
5
@DocBrown Dies ist auch der Grund, Stringder auch finalin Java markiert ist .
Dev
47
"Als Java entwickelt wurde, galt Smalltalk als zu langsam […]. Java opferte einige Eleganz und konzeptionelle Reinheit, um eine bessere Leistung zu erzielen." - Ironischerweise hat Java diese Leistung natürlich erst erreicht, als Sun eine Smalltalk-Firma kaufte, um auf die Smalltalk-VM-Technologie zuzugreifen, da die eigene JVM von Sun sehr langsam war und die HotSpot-JVM, eine leicht modifizierte Smalltalk-VM, herausbrachte.
Jörg W Mittag
3
@underscore_d: Die Antwort , die Sie sehr explizit verknüpft besagt , dass C♯ ist nicht primitive Typen haben. Sicher, einige Plattformen, für die eine Implementierung von C♯ existiert, können primitive Typen haben oder auch nicht, aber das bedeutet nicht, dass C♯ primitive Typen hat. ZB gibt es eine Implementierung von Ruby für die CLI, und die CLI hat primitive Typen, was jedoch nicht bedeutet, dass Ruby primitive Typen hat. Die Implementierung kann sich dafür entscheiden, Werttypen zu implementieren, indem sie den primitiven Typen der Plattform zugeordnet werden. Dies ist jedoch ein privates internes Implementierungsdetail und nicht Teil der Spezifikation.
Jörg W Mittag
10
Es geht nur um Abstraktion. Wir müssen den Kopf freihalten, sonst bekommen wir Unsinn. Zum Beispiel: C♯ ist in .NET implementiert. .NET ist unter Windows NT implementiert. Windows NT ist auf x86 implementiert. x86 ist auf Silikondioxid implementiert. SiO₂ ist nur Sand. Also ist ein stringin C♯ nur Sand? Nein, natürlich nicht, ein stringin 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- Strings abbilden usw.
Jörg W Mittag
20

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:

 type Angle is range -10 .. 10;
 type Hours is range 0 .. 23; 

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).

 type Reference is Integer;
 type Count is Integer;

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.)

Core-Dump
quelle
2
Eigentlich denke ich, dass es mehr um Semantik geht. Die Verwendung einer Größe, für die ein Index erwartet wird, würde dann hoffentlich einen Fehler bei der Kompilierung verursachen
Marjan Venema,
@MarjanVenema Das ist der Fall, und dies geschieht absichtlich, um logische Fehler abzufangen.
Coredump
Mein Punkt war, dass Sie nicht in allen Fällen, in denen Sie die Semantik wollen, die Bereiche brauchen würden. Sie hätten dann, 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?
Marjan Venema
1
@MarjanVenema In ihrem zweiten Beispiel sind beide Typen Untertypen von Integer. Wenn Sie jedoch eine Funktion deklarieren , die eine Zählung akzeptiert, können Sie nicht eine Referenz übergeben , da die Typprüfung auf Basis Name Gleichwertigkeit , die das Gegenteil von dem ist „alles , was geprüft wird , sind die Bereiche“. Dies ist nicht auf Ganzzahlen beschränkt. Sie können Aufzählungstypen oder Datensätze verwenden. ( archive.adaic.com/standards/83rat/html/ratl-04-03.html )
Coredump
1
@Marjan Ein gutes Beispiel dafür, warum Tagging-Typen sehr leistungsfähig sein können, finden Sie in Eric Lipperts Serie zur Implementierung von Zorg in OCaml . Auf diese Weise kann der Compiler viele Fehler abfangen. Wenn Sie jedoch zulassen, dass Typen implizit konvertiert werden, scheint dies die Funktion unbrauchbar zu machen. Es macht keinen semantischen Sinn, einen PersonAge-Typ nur einem PersonId-Typ zuweisen zu können weil beide zufällig den gleichen zugrunde liegenden Typ haben.
Voo
16

Ich denke, das könnte eine X / Y-Frage sein. Wichtige Punkte aus der Frage ...

Meine Motivation ist es, die Verwendung des Typsystems für die Überprüfung der Kompilierzeit auf Korrektheit zu maximieren.

... und aus deinem Kommentar heraus :

Ich möchte nicht in der Lage sein, einen implizit durch einen anderen zu ersetzen.

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 intin Bezug auf Reichweite und Repräsentation, aber nicht in Kontexte zu ersetzen ist, die ein erwarten intund 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 :

Es gibt das hinzugefügte Problem, dass, wenn jemand in der Lage wäre, von Int zu erben, Sie unschuldigen Code wie "int x = y + 2" (wobei Y die abgeleitete Klasse ist) haben, der jetzt ein Protokoll in die Datenbank schreibt, eine URL öffnet und irgendwie Elvis wieder auferstehen. Primitive Typen sollen sicher sein und ein mehr oder weniger definiertes Verhalten aufweisen.

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.

underscore_d
quelle
4

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 intas-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 sind int? 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.

Stringist ein anderer Fall. Sowohl in Java als auch in C # Stringist 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 # -Standardbibliothek Strings 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.

Polygnom
quelle
1
Diese Antwort ist unglaublich ... eng. 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 mit dynamic_castoder typeid. Und selbst wenn RTTI aktiviert ist, belegt die Vererbung nur dann Speicherplatz, wenn eine Klasse über virtualMethoden verfügt , auf die eine klassenbezogene Methodentabelle pro Instanz
verweisen
1
Die Vererbung in C ++ funktioniert ganz anders als in Sprachen, die auf einer VM ausgeführt werden. Für den virtuellen Versand ist RTTI erforderlich, was ursprünglich nicht Teil von C ++ war. Die Vererbung ohne virtuellen Versand ist sehr begrenzt, und ich bin mir nicht sicher, ob Sie sie mit der Vererbung mit virtuellem Versand vergleichen sollten. Darüber hinaus unterscheidet sich der Begriff "Objekt" in C ++ erheblich von dem in C # oder Java. Sie haben Recht, es gibt einige Dinge, die ich besser formulieren könnte, aber wenn Sie sich mit all den ziemlich komplizierten Punkten befassen, müssen Sie schnell ein Buch über Sprachdesign schreiben.
Polygnome
3
Es ist auch nicht der Fall, dass "Virtual Dispatch RTTI erfordert" in C ++. Wieder nur dynamic_castund typeinfoverlangen 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.
Underscore_d
2
Umgekehrt erfordert RTTI einen virtuellen Versand. Wörtlich erlaubt -C ++ keine dynamic_castKlassen ohne virtuellen Versand. Der Grund für die Implementierung ist, dass RTTI im Allgemeinen als verborgenes Mitglied einer vtable implementiert wird.
MSalters
1
@MilesRout C ++ hat alles, was eine Sprache für OOP benötigt, zumindest die etwas neueren Standards. Man könnte argumentieren, dass den älteren C ++ - Standards einige Dinge fehlen, die für eine OOP-Sprache benötigt werden, aber selbst das ist eine Strecke. C ++ ist keine OOP-Sprache auf hoher Ebene , da es eine direktere Kontrolle auf niedriger Ebene über einige Dinge ermöglicht, aber es ermöglicht trotzdem OOP. (High Level / Low Level hier in Bezug auf Abstraktion , andere Sprachen wie verwaltete abstrahieren mehr vom System als C ++, daher ist ihre Abstraktion höher).
Polygnome
4

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 ShapeInstanz verwendet, muss zuerst auf die Typinformationen dieser Instanz zugreifen, bevor er die richtige Area()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:

Warum verhindern die starken statischen OOP-Sprachen des Mainstreams das Erben von Primitiven?

Ist:

  • Es gab wenig Nachfrage
  • Und es hätte die Sprache zu langsam gemacht
  • Die Untertypisierung wurde in erster Linie als Möglichkeit gesehen, einen Typ zu erweitern, anstatt eine bessere (benutzerdefinierte) statische Typprüfung zu erhalten.

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.

Ian
quelle
F # -Maßeinheiten sind eine nette Funktion, obwohl sie leider falsch benannt sind. Außerdem ist es nur zur Kompilierungszeit verfügbar und daher nicht besonders nützlich, wenn Sie z. B. ein kompiliertes NuGet-Paket verwenden. Richtige Richtung.
Den
Es ist vielleicht interessant zu bemerken, dass "dimension" keine andere Eigenschaft als "type" ist, sondern nur eine reichhaltigere Art von Typ, als Sie es vielleicht gewohnt sind.
Porglezomp
3

Ich bin mir nicht sicher, ob ich hier etwas übersehen habe, aber die Antwort ist ziemlich einfach:

  1. Die Definition von Grundelementen lautet: Grundelementwerte sind keine Objekte, Grundelementtypen sind keine Objekttypen, Grundelemente sind kein Teil des Objektsystems.
  2. Vererbung ist ein Merkmal des Objektsystems.
  3. Ergo können Primitive nicht an der Vererbung teilnehmen.

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 ( structs), die dennoch Objekte sind.

Eine andere Sache ist, dass das Nicht-Erben auch nicht nur für Primitive gilt. In C♯ können structs implizit s erben System.Objectund implementieren interface, sie können jedoch weder von es noch von s erben classoder erben struct. Auch sealed classkann es nicht vererbt werden. In Java kann final classes nicht von geerbt werden.

tl; dr :

Warum verhindern die starken statischen OOP-Sprachen des Mainstreams das Erben von Primitiven?

  1. Primitive sind nicht Teil des Objektsystems (wenn sie es wären, wären sie per definitionem nicht primitiv), die Idee der Vererbung ist an das Objektsystem gebunden, die ergo-primitive Vererbung ist ein Widerspruch
  2. Grundelemente sind nicht eindeutig, viele andere Typen können auch nicht vererbt werden ( finaloder sealedin Java oder C♯, structs in C♯, case classes in Scala)
Jörg W. Mittag
quelle
3
Ähm ... Ich weiß, dass es "C Sharp" ausgesprochen wird, aber, ehm
Mr Lister
Ich denke, Sie irren sich ziemlich in C ++. Es ist überhaupt keine reine OO-Sprache. Klassenmethoden sind nicht standardmäßig virtual, was bedeutet, dass sie nicht LSP gehorchen. ZB std::stringist 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.
MSalters
2
"In Java sind Grundelemente das Ergebnis eines fehlgeleiteten Versuchs, die Leistung zu verbessern." Ich glaube, Sie haben keine Ahnung, inwieweit die Implementierung von Primitiven als vom Benutzer erweiterbare Objekttypen zu Leistungseinbußen führt. Diese Entscheidung in Java ist absichtlich und begründet. Stellen Sie sich vor, Sie müssen für jeden intverwendeten 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 wird int. Ihre Java-Codes würden mitkriechen, wenn die Designer der Sprache etwas anderes entschieden hätten.
cmaster
1
@cmaster: Scala hat keine Grundelemente und seine numerische Leistung ist genau die gleiche wie die von Java. Weil es ganze Zahlen in JVM-Grundelemente kompiliert int, führen sie also genau dasselbe aus. (Scala-native kompiliert sie in primitive Maschinenregister, Scala.js kompiliert sie in primitive ECMAScript-Dateien Number.) Ruby hat keine Primitive, aber YARV und Rubinius kompilieren Ganzzahlen in primitive Maschinen-Ganzzahlen, JRuby kompiliert sie in JVM-Primitive long. Nahezu jede Lisp-, Smalltalk- oder Ruby-Implementierung verwendet Primitive in der VM . Hier werden Leistungsoptimierungen durchgeführt…
Jörg W Mittag
1
… Gehören: in den Compiler, nicht die Sprache.
Jörg W Mittag
2

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.

CodesInTheDark
quelle
2

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) stringin einer objectVariablen speichern , ohne die Informationen zu verlieren, die ihn zu einem machen string. 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.

Theodoros Chatzigiannakis
quelle
1

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!

Beschmutzt
quelle
Der Link ist eine interessante Designmeinung. Braucht mehr Nachdenken für mich.
Den
1

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 + Indexmacht 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.

Karl Bielefeldt
quelle