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.
declare type Int
declare type String
Dann können wir verschiedene Werte als solche bezeichnen.
0 is of type Int
1 is of type Int
-1 is of type Int
...
"" is of type String
"a" is of type String
"b" is of type String
...
Mit diesen Aussagen kann unser Typechecker nun Aussagen wie ablehnen
0 is of type String
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).
class StringClass:
defMethod concatenate(otherString): ...
defField size: ...
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.
associate StringClass with String
Aber nicht jedem Typ muss eine Klasse zugeordnet sein.
# Hmm... Doesn't look like there's a class for Int
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.
associate MyCustomStringClass with String
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 hatten 0
. Es hatte keine besonderen Kenntnisse über den Wert von 0
.
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 verlangt B
, wird er auch ein akzeptieren A
.
Zum Beispiel könnten wir das Folgende für unsere Zahlen machen, anstatt was wir vorher hatten.
declare type NaturalNum
declare type Int
NaturalNum is subtype of Int
0 is of type NaturalNum
1 is of type NaturalNum
-1 is of type Int
...
Unterklassen sind eine Abkürzung zum Deklarieren einer neuen Klasse, mit der Sie zuvor deklarierte Methoden und Felder wiederverwenden können.
class ExtendedStringClass is subclass of StringClass:
# We get concatenate and size for free!
def addQuestionMark: ...
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 Typ String
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 sein
associate NewClass with String
Jede Instanz Jetzt StringClass
können substituiert sein NewClass
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 keine concatenate
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 wir associate
die verschiedenen Namen zwischen StringClass
und loswerden String
.
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 loswerden is 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.