Die bestbewertete Antwort auf diese Frage nach dem Liskov-Substitutionsprinzip erfordert die Unterscheidung zwischen den Begriffen Untertyp und Unterklasse . Es wird auch darauf hingewiesen, dass einige Sprachen die beiden Sprachen miteinander verbinden, andere jedoch nicht.
Für die objektorientierten Sprachen, die ich am besten kenne (Python, C ++), sind "Typ" und "Klasse" synonym. Was würde es in Bezug auf C ++ bedeuten, zwischen Untertyp und Unterklasse zu unterscheiden? Sagen wir zum Beispiel, das Foo
ist eine Unterklasse, aber kein Untertyp von FooBase
. Wenn foo
eine Instanz von ist Foo
, würde diese Zeile:
FooBase* fbPoint = &foo;
nicht mehr gültig sein?
Antworten:
Subtypisierung ist eine Form des Typpolymorphismus, bei der ein Subtyp ein Datentyp ist, der in gewisser Weise mit einem anderen Datentyp (dem Supertyp) verwandt ist, was bedeutet, dass Programmelemente, typischerweise Subroutinen oder Funktionen, die für die Bearbeitung von Elementen des Supertyps geschrieben wurden, dies auch können Arbeiten Sie mit Elementen des Subtyps.
Wenn
S
es sich um einen Subtyp handeltT
, wird häufig die Subtyp- Beziehung geschriebenS <: T
, um zu bedeuten, dass jeder Begriff des TypsS
in einem Kontext sicher verwendet werden kann, in dem ein Begriff des TypsT
erwartet wird. Die genaue Semantik der Subtypisierung hängt entscheidend von den Einzelheiten dessen ab, was "sicher in einem Kontext wo verwendet" in einer gegebenen Programmiersprache bedeutet.Unterklassen sollten nicht mit Untertypen verwechselt werden. Im Allgemeinen stellt die Subtypisierung eine is-a-Beziehung her, während die Subklasse nur die Implementierung wiederverwendet und eine syntaktische Beziehung herstellt, nicht notwendigerweise eine semantische Beziehung (die Vererbung gewährleistet nicht die Subtypisierung des Verhaltens).
Zur Unterscheidung dieser Konzepte wird die Untertypisierung auch als Schnittstellenvererbung bezeichnet , während die Unterklasse als Implementierungsvererbung oder Codevererbung bezeichnet wird.
Referenzen
Subtyping
Inheritance
quelle
public
Vererbung in C ++ einen Untertyp ein, während dieprivate
Vererbung eine Unterklasse einführt.Ein Typus ist in dem hier angesprochenen Kontext im Wesentlichen eine Reihe von Verhaltensgarantien. Ein Vertrag , wenn Sie so wollen. Oder Ausleihen von Terminologie von Smalltalk, einem Protokoll .
Eine Klasse ist ein Bündel von Methoden. Es ist eine Reihe von Verhaltensimplementierungen .
Subtypisierung ist ein Mittel zur Verfeinerung des Protokolls. Unterklassen sind Mittel zur Wiederverwendung von Differenzialcode, dh zur Wiederverwendung von Code, indem nur der Unterschied im Verhalten beschrieben wird.
Wenn Sie Java oder C♯ verwendet haben, sind Sie möglicherweise auf den Rat gestoßen, dass alle Typen Typen sein sollten
interface
. Wenn Sie William Cook lesen In der Tat, auf das Verständnis Data Abstraction, Revisited , dann können Sie wissen , dass , um OO in diesen Sprachen zu tun, Sie müssen nur Gebrauchinterface
s als Typen. (Auch eine lustige Tatsache: Java hatinterface
s direkt von Objective- Cs Protokollen abgeschnitten , die wiederum direkt von Smalltalk stammen.)Befolgen wir nun diesen Codierungshinweis zu seiner logischen Schlussfolgerung und stellen uns eine Java-Version vor, in der nur
interface
s Typen sind und Klassen und Primitive nicht, dann erzeugt eininterface
Erben von einem anderen eine Untertypisierungsbeziehung, während einclass
Erben von einem anderen Willen erfolgt nur zur differenziellen Code-Wiederverwendung übersuper
.Soweit mir bekannt ist, gibt es keine gängigen statisch typisierten Sprachen, die streng zwischen der Vererbung von Code (Vererbung von Implementierungen / Unterklassen) und der Vererbung von Verträgen (Untertypen) unterscheiden. In Java und C♯ ist die Schnittstellenvererbung eine reine Untertypisierung (oder zumindest bis zur Einführung von Standardmethoden in Java 8 und wahrscheinlich auch in C♯ 8), aber die Klassenvererbung ist auch eine Untertypisierung sowie eine Implementierungsvererbung. Ich erinnere mich an einen experimentellen statisch typisierten objektorientierten LISP-Dialekt, der streng zwischen Mixins (die Verhalten enthalten ), Strukturen (die Zustand enthalten) und Interfaces (die beschreiben) unterschieden hatVerhalten) und Klassen (die null oder mehr Strukturen mit einem oder mehreren Mixins zusammensetzen und einer oder mehreren Schnittstellen entsprechen). Es können nur Klassen instanziiert und nur Schnittstellen als Typen verwendet werden.
In einer dynamisch typisierten OO-Sprache wie Python, Ruby, ECMAScript oder Smalltalk stellen wir uns den Typ (die Typen) eines Objekts im Allgemeinen als den Satz von Protokollen vor, denen es entspricht. Beachten Sie den Plural: Ein Objekt kann mehrere Typen haben, und ich spreche nicht nur von der Tatsache, dass jedes Objekt vom Typ
String
auch ein Objekt vom Typ istObject
. (Übrigens: Beachten Sie, wie ich Klassennamen verwendet habe, um über Typen zu sprechen? Wie dumm von mir!) Ein Objekt kann mehrere Protokolle implementieren. In RubyArrays
können sie beispielsweise angehängt, indiziert, durchlaufen und verglichen werden. Das sind vier verschiedene Protokolle, die sie implementieren!Ruby hat keine Typen. Aber die Ruby- Community hat Typen! Sie existieren jedoch nur in den Köpfen der Programmierer. Und in der Dokumentation. Beispielsweise wird jedes Objekt, das auf eine aufgerufene Methode antwortet,
each
indem es seine Elemente einzeln ausgibt , als ein aufzählbares Objekt betrachtet. Und es gibt ein Mixin,Enumerable
das von diesem Protokoll abhängt . Also, wenn Ihr Objekt den richtigen hat Typen (die nur im Programmierkopf vorhanden ist ), dann ist es erlaubt , in (vererben) dem mischenEnumerable
mixin, und es auch all möglichen kühlen Methoden kostenlos bekommen, wiemap
,reduce
,filter
und so auf.Ebenso, wenn ein Objekt reagiert
<=>
, dann wird es als das implementieren vergleichbare Protokoll, und es kann in der MischungComparable
wie mixin und bekommen Sachen<
,<=
,>
,<=
,==
,between?
, undclamp
kostenlos. Es kann jedoch auch alle diese Methoden selbst implementieren und überhaupt nicht erbenComparable
, und es würde immer noch als vergleichbar angesehen .Ein gutes Beispiel ist die
StringIO
Bibliothek, die im Wesentlichen I / O-Streams mit Strings fälscht . Es werden alle Methoden derIO
Klasse implementiert , es besteht jedoch keine Vererbungsbeziehung zwischen den beiden. TrotzdemStringIO
kann a überall eingesetzt werden undIO
kann eingesetzt werden. Dies ist sehr nützlich bei Unit-Tests, bei denen Sie eine Datei ersetzen oderstdin
durch eine ersetzen können,StringIO
ohne weitere Änderungen an Ihrem Programm vornehmen zu müssen. Da esStringIO
sich um dasselbe Protokoll wie handeltIO
, handelt es sich bei beiden um denselben Typ, auch wenn es sich um unterschiedliche Klassen handelt, und sie haben keine gemeinsame Beziehung (abgesehen von der Trivialität, die beideObject
zu einem bestimmten Zeitpunkt aufweisen).quelle
Es ist vielleicht zuerst nützlich, zwischen einem Typ und einer Klasse zu unterscheiden und dann auf den Unterschied zwischen Untertypen und Unterklassen einzugehen.
Für den Rest dieser Antwort gehe ich davon aus, dass es sich bei den diskutierten Typen um statische Typen handelt (da die Untertypisierung normalerweise in einem statischen Kontext erfolgt).
Ich werde einen Spielzeug-Pseudocode entwickeln, um den Unterschied zwischen einem Typ und einer Klasse zu veranschaulichen, da die meisten Sprachen sie zumindest teilweise zusammenführen (aus gutem Grund, auf den ich kurz eingehen werde).
Beginnen wir mit einem Typ. Ein Typ ist eine Bezeichnung für einen Ausdruck in Ihrem Code. Der Wert dieser Beschriftung und ob er (für einige typsystemspezifische Definitionen von konsistent) mit dem Wert aller anderen Beschriftungen übereinstimmt, kann von einem externen Programm (einem Typechecker) ermittelt werden, ohne dass das Programm ausgeführt werden muss. Das ist es, was diese Labels zu etwas Besonderem macht und ihren eigenen Namen verdient.
In unserer Spielzeugsprache können wir möglicherweise Labels wie dieses erstellen.
Dann können wir verschiedene Werte als solche bezeichnen.
Mit diesen Aussagen kann unser Typechecker nun Aussagen wie ablehnen
wenn eine der Anforderungen unseres Typsystems ist, dass jeder Ausdruck einen eindeutigen Typ hat.
Lassen Sie uns zunächst einmal beiseite, wie klobig dies ist und wie Sie Probleme haben werden, eine unendliche Anzahl von Ausdruckstypen zuzuweisen. Wir können später darauf zurückkommen.
Eine Klasse hingegen ist eine Sammlung von Methoden und Feldern, die zusammen gruppiert sind (möglicherweise mit Zugriffsmodifikatoren wie privat oder öffentlich).
Eine Instanz dieser Klasse kann bereits vorhandene Definitionen dieser Methoden und Felder erstellen oder verwenden.
Wir können eine Klasse einem Typ zuordnen, sodass jede Instanz einer Klasse automatisch mit diesem Typ gekennzeichnet wird.
Aber nicht jedem Typ muss eine Klasse zugeordnet sein.
Es ist auch denkbar, dass in unserer Spielzeugsprache nicht jede Klasse einen Typ hat, besonders wenn nicht alle unsere Ausdrücke Typen haben. Es ist etwas kniffliger (aber nicht unmöglich), sich vorzustellen, wie Konsistenzregeln für Typsysteme aussehen würden, wenn einige Ausdrücke Typen hätten und andere nicht.
Außerdem müssen diese Assoziationen in unserer Spielzeugsprache nicht eindeutig sein. Wir könnten zwei Klassen demselben Typ zuordnen.
Denken Sie jetzt daran, dass unsere Schreibmaschine den Wert eines Ausdrucks nicht nachverfolgen muss (und in den meisten Fällen ist dies nicht oder nicht möglich). Alles, was es weiß, sind die Etiketten, die Sie gesagt haben. Zur Erinnerung: Bisher konnte der Typechecker die Aussage nur
0 is of type String
aufgrund unserer künstlich erzeugten Typregel ablehnen, dass Ausdrücke eindeutige Typen haben müssen und wir den Ausdruck bereits mit einer anderen Bezeichnung versehen hatten0
. Es hatte keine besonderen Kenntnisse über den Wert von0
.Was ist mit Subtypisierung? Die Untertypisierung ist ein Name für eine allgemeine Regel in der Typüberprüfung, die die anderen Regeln, die Sie möglicherweise haben, entspannt. Nämlich, wenn
A is subtype of B
Ihr Typechecker dann überall ein Etikett von verlangtB
, wird er auch ein akzeptierenA
.Zum Beispiel könnten wir das Folgende für unsere Zahlen machen, anstatt was wir vorher hatten.
Unterklassen sind eine Abkürzung zum Deklarieren einer neuen Klasse, mit der Sie zuvor deklarierte Methoden und Felder wiederverwenden können.
Wir müssen keine Instanzen von
ExtendedStringClass
mit verknüpfen,String
wie wir es getan haben,StringClass
da es ja eine ganz neue Klasse ist, wir mussten einfach nicht so viel schreiben. Dies würde es uns ermöglichen,ExtendedStringClass
einen TypString
anzugeben, der aus Sicht des Typcheckers nicht kompatibel ist.Ebenso hätten wir uns entschließen können, eine ganz neue Klasse zu bilden
NewClass
und fertig zu seinJede Instanz Jetzt
StringClass
können substituiert seinNewClass
aus der typechecker Sicht.Theoretisch sind Subtypisierung und Subklassifizierung also völlig verschiedene Dinge. Aber keine Sprache, die ich kenne, hat Typen und Klassen, die Dinge tatsächlich so machen. Lassen Sie uns anfangen, unsere Sprache zu reduzieren und die Gründe für einige unserer Entscheidungen zu erklären.
Erstens, obwohl theoretisch völlig verschiedene Klassen den gleichen Typ oder eine Klasse den gleichen Typ wie Werte erhalten könnten, die keine Instanzen einer Klasse sind, beeinträchtigt dies die Nützlichkeit des Typecheckers erheblich. Dem Typechecker wird effektiv die Möglichkeit genommen, zu überprüfen, ob die in einem Ausdruck aufgerufene Methode oder das aufgerufene Feld tatsächlich für diesen Wert vorhanden ist. Dies ist wahrscheinlich eine Überprüfung, die Sie wünschen, wenn Sie sich die Mühe machen, mit einem zu spielen typechecker. Wer weiß schon, welchen Wert das
String
Etikett tatsächlich hat? es könnte etwas sein, das zB überhaupt keineconcatenate
Methode hat!Angenommen, jede Klasse generiert automatisch einen neuen Typ mit demselben Namen wie diese Klasse und die
associate
Instanzen mit diesem Typ. So können wirassociate
die verschiedenen Namen zwischenStringClass
und loswerdenString
.Aus dem gleichen Grund möchten wir wahrscheinlich automatisch eine Untertypbeziehung zwischen den Typen von zwei Klassen herstellen, wobei eine eine Unterklasse einer anderen ist. Schließlich verfügt die Unterklasse garantiert über alle Methoden und Felder, die die übergeordnete Klasse verwendet. Das Gegenteil ist jedoch nicht der Fall. Während die Unterklasse jedes Mal übergeben werden kann, wenn Sie einen Typ der übergeordneten Klasse benötigen, sollte der Typ der übergeordneten Klasse abgelehnt werden, wenn Sie den Typ der Unterklasse benötigen.
Wenn Sie dies mit der Bedingung kombinieren, dass alle benutzerdefinierten Werte Instanzen einer Klasse sein müssen, können Sie die
is subclass of
doppelte Aufgabe ausführen und loswerdenis subtype of
.Und das bringt uns zu den Merkmalen, die die meisten der beliebten statisch typisierten OO-Sprachen gemeinsam haben. Es gibt eine Reihe von „primitiven“ Typen (zB
int
,float
usw.), die nicht mit jeder Klasse zugeordnet und sind nicht benutzerdefiniert. Dann haben Sie alle benutzerdefinierten Klassen, die automatisch gleichnamige Typen haben und Unterklassen mit Untertypen identifizieren.Die letzte Anmerkung, die ich machen werde, handelt von der Schwierigkeit, Typen getrennt von Werten zu deklarieren. Die meisten Sprachen verknüpfen die Erstellung der beiden, sodass eine Typdeklaration auch eine Deklaration zum Generieren völlig neuer Werte ist, die automatisch mit diesem Typ beschriftet werden. Beispielsweise erstellt eine Klassendeklaration in der Regel sowohl den Typ als auch eine Methode zum Instanziieren von Werten dieses Typs. Dadurch wird ein Teil der Unübersichtlichkeit beseitigt, und in Gegenwart von Konstruktoren können Sie mit einem Typ in einem Strich unendlich viele Werte beschriften.
quelle