Was ist der Unterschied zwischen einer Unterklasse und einem Subtyp?

44

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 Fooist eine Unterklasse, aber kein Untertyp von FooBase. Wenn fooeine Instanz von ist Foo, würde diese Zeile:

FooBase* fbPoint = &foo;

nicht mehr gültig sein?

tel
quelle
6
In Python sind "type" und "class" unterschiedliche Konzepte. Da Python dynamisch eingegeben wird, ist "type" in Python überhaupt kein Konzept . Leider versteht der Python - Entwickler nicht , dass und noch conflate die beide.
Jörg W Mittag
11
"Typ" und "Klasse" unterscheiden sich auch in C ++. "Array of ints" ist ein Typ; welche klasse ist das "Zeiger auf eine Variable vom Typ int" ist ein Typ; welche klasse ist das Diese Dinge sind keine Klasse, aber sie sind sicherlich Typen.
Eric Lippert
2
Ich habe mich genau das gefragt, nachdem ich diese Frage und diese Antwort gelesen hatte.
user369450
4
@JorgWMittag Wenn es in Python kein Konzept für einen "Typ" gibt, sollte jemand sagen, wer die Dokumentation schreibt: docs.python.org/3/library/stdtypes.html
Matt
@Matt um fair zu sein, Typen haben es in 3.5 geschafft, was ziemlich neu ist, besonders nach dem, was ich in der Produktion verwenden darf.
Jared Smith

Antworten:

53

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 Ses sich um einen Subtyp handelt T, wird häufig die Subtyp- Beziehung geschrieben S <: T, um zu bedeuten, dass jeder Begriff des Typs Sin einem Kontext sicher verwendet werden kann, in dem ein Begriff des Typs Terwartet 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

Robert Harvey
quelle
1
Sehr gut gesagt. Im Zusammenhang mit der Frage kann erwähnenswert sein, dass C ++ - Programmierer häufig reine virtuelle Basisklassen verwenden, um dem Typsystem Subtypisierungsbeziehungen mitzuteilen. Natürlich werden häufig generische Programmieransätze bevorzugt.
Aluan Haddad
6
"Die genaue Semantik der Subtypisierung hängt entscheidend von den Einzelheiten dessen ab, was" in einem Kontext sicher verwendet "in einer gegebenen Programmiersprache bedeutet." … Und der LSP definiert eine einigermaßen vernünftige Vorstellung davon, was "sicher" bedeutet, und gibt an, welche Einschränkungen diese Angaben erfüllen müssen, um diese bestimmte Form von "Sicherheit" zu ermöglichen.
Jörg W Mittag
Noch ein Beispiel auf dem Stapel: Wenn ich richtig verstanden habe, führt die publicVererbung in C ++ einen Untertyp ein, während die privateVererbung eine Unterklasse einführt.
Quentin
Die öffentliche Vererbung von @Quentin ist sowohl ein Subtyp als auch eine Subklasse, aber privat ist nur eine Subklasse, aber kein Subtyp. Sie haben ohne Subtypisierung mit Strukturen wie Java - Schnittstellen Subklassen
eques
27

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 Gebrauch interfaces als Typen. (Auch eine lustige Tatsache: Java hat interfaces 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 ein interfaceErben von einem anderen eine Untertypisierungsbeziehung, während ein classErben von einem anderen Willen erfolgt nur zur differenziellen Code-Wiederverwendung über super.

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 Stringauch ein Objekt vom Typ ist Object. (Übrigens: Beachten Sie, wie ich Klassennamen verwendet habe, um über Typen zu sprechen? Wie dumm von mir!) Ein Objekt kann mehrere Protokolle implementieren. In Ruby Arrayskö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, eachindem es seine Elemente einzeln ausgibt , als ein aufzählbares Objekt betrachtet. Und es gibt ein Mixin, Enumerabledas 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 mischen Enumerablemixin, und es auch all möglichen kühlen Methoden kostenlos bekommen, wie map, reduce, filterund so auf.

Ebenso, wenn ein Objekt reagiert <=>, dann wird es als das implementieren vergleichbare Protokoll, und es kann in der Mischung Comparablewie mixin und bekommen Sachen <, <=, >, <=, ==, between?, und clampkostenlos. Es kann jedoch auch alle diese Methoden selbst implementieren und überhaupt nicht erben Comparable, und es würde immer noch als vergleichbar angesehen .

Ein gutes Beispiel ist die StringIOBibliothek, die im Wesentlichen I / O-Streams mit Strings fälscht . Es werden alle Methoden der IOKlasse implementiert , es besteht jedoch keine Vererbungsbeziehung zwischen den beiden. Trotzdem StringIOkann a überall eingesetzt werden und IOkann eingesetzt werden. Dies ist sehr nützlich bei Unit-Tests, bei denen Sie eine Datei ersetzen oder stdindurch eine ersetzen können, StringIOohne weitere Änderungen an Ihrem Programm vornehmen zu müssen. Da es StringIOsich um dasselbe Protokoll wie handelt IO, 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 beide Objectzu einem bestimmten Zeitpunkt aufweisen).

Jörg W. Mittag
quelle
Es kann hilfreich sein, wenn Sprachen zulassen, dass Programme gleichzeitig einen Klassentyp und eine Schnittstelle deklarieren, für die diese Klasse eine Implementierung ist, und Implementierungen die Angabe von "Konstruktoren" zulassen (die mit Konstruktoren von Klassen verkettet werden, die von der Schnittstelle angegeben werden). Für Objekttypen, für die Verweise öffentlich freigegeben werden, ist das bevorzugte Muster, dass der Klassentyp nur beim Erstellen abgeleiteter Klassen verwendet wird. Die meisten Referenzen sollten vom Schnittstellentyp sein. Die Angabe von Schnittstellenkonstruktoren wäre hilfreich in Situationen, in denen ...
supercat
... zB Code benötigt eine Auflistung, mit der bestimmte Werte vom Index gelesen werden können, aber es ist egal, um welchen Typ es sich handelt. Obwohl es gute Gründe gibt, Klassen und Interfaces als unterschiedliche Arten von Typen zu erkennen, gibt es viele Situationen, in denen sie in der Lage sein sollten, enger zusammenzuarbeiten, als es die Sprachen derzeit zulassen.
Supercat
Haben Sie eine Referenz oder Schlüsselwörter, nach denen ich nach weiteren Informationen über den von Ihnen erwähnten experimentellen LISP-Dialekt suchen könnte, der Mixins, Strukturen, Interfaces und Klassen formal unterscheidet?
Tel.
@tel: Nein, sorry. Es war wahrscheinlich vor ungefähr 15 bis 20 Jahren, und zu dieser Zeit waren meine Interessen allgegenwärtig. Ich konnte Ihnen unmöglich sagen, wonach ich suchte, als ich darüber stolperte.
Jörg W Mittag
Awww. Das war das interessanteste Detail in all diesen Antworten. Die Tatsache, dass eine formale Trennung dieser Konzepte innerhalb der Implementierung einer Sprache tatsächlich möglich ist, hat mir wirklich geholfen, die Klassen- / Typunterscheidung zu kristallisieren. Ich schätze, ich werde das LISP auf jeden Fall selbst suchen. Erinnern Sie sich zufällig, ob Sie darüber in einem Zeitschriftenartikel / Buch gelesen oder nur im Gespräch davon gehört haben?
Tel.
2

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 Stringaufgrund 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 BIhr 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 ExtendedStringClassmit verknüpfen, Stringwie wir es getan haben, StringClassda es ja eine ganz neue Klasse ist, wir mussten einfach nicht so viel schreiben. Dies würde es uns ermöglichen, ExtendedStringClasseinen Typ Stringanzugeben, der aus Sicht des Typcheckers nicht kompatibel ist.

Ebenso hätten wir uns entschließen können, eine ganz neue Klasse zu bilden NewClassund fertig zu sein

associate NewClass with String

Jede Instanz Jetzt StringClasskönnen substituiert sein NewClassaus 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 StringEtikett tatsächlich hat? es könnte etwas sein, das zB überhaupt keine concatenateMethode hat!

Angenommen, jede Klasse generiert automatisch einen neuen Typ mit demselben Namen wie diese Klasse und die associateInstanzen mit diesem Typ. So können wir associatedie verschiedenen Namen zwischen StringClassund 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 ofdoppelte 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, floatusw.), 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.

badcook
quelle