Jaka jest różnica między podklasą a podtypem?

44

Najwyżej oceniana odpowiedź na to pytanie dotyczące zasady substytucji Liskowa stara się rozróżnić pojęcia podtyp i podklasa . Wskazuje również, że niektóre języki łączą oba języki, podczas gdy inne nie.

W przypadku języków obiektowych, które znam najbardziej (Python, C ++), „typ” i „klasa” są pojęciami synonimicznymi. Jeśli chodzi o C ++, co oznaczałoby rozróżnienie podtypu i podklasy? Powiedzmy na przykład, że Foojest to podklasa, ale nie podtyp FooBase. Jeśli foojest instancją Foo, czy ta linia:

FooBase* fbPoint = &foo;

nie jest już ważny?

tel
źródło
6
W rzeczywistości w Pythonie „typ” i „klasa” to odrębne pojęcia. W rzeczywistości, Python jest dynamicznie wpisane, „typ” nie jest pojęciem w ogóle w Pythonie. Niestety programiści Python nie rozumieją tego i nadal łączą te dwa elementy.
Jörg W Mittag,
11
„Typ” i „klasa” są również odrębne w C ++. „Array of ints” to typ; jaka to klasa? „wskaźnik do zmiennej typu int” to typ; jaka to klasa? Te rzeczy nie są żadną klasą, ale z pewnością są typami.
Eric Lippert,
2
Zastanawiałem się nad tym właśnie po przeczytaniu tego pytania i tej odpowiedzi.
user369450,
4
@JorgWMittag Jeśli nie ma pojęcia „typu” w pythonie, ktoś powinien powiedzieć, kto pisze dokumentację: docs.python.org/3/library/stdtypes.html
Matt
@Matt, aby być uczciwym, typy pojawiły się w wersji 3.5, która jest całkiem niedawno, szczególnie według standardów dozwolonych w produkcji.
Jared Smith

Odpowiedzi:

53

Podtyp jest formą polimorfizmu typu, w którym podtyp jest typem danych, który jest powiązany z innym typem danych (nadtypem) przez pewne pojęcie podstawialności, co oznacza, że ​​elementy programu, zwykle podprogramy lub funkcje, napisane w celu działania na elementach nadtypu mogą również działają na elementach podtypu.

Jeśli Sjest podtypem T, relacja podtypu jest często zapisywana S <: T, co oznacza, że ​​dowolny termin typu Smoże być bezpiecznie używany w kontekście, w którym Toczekiwany jest termin typu . Dokładna semantyka podtytułu zależy przede wszystkim od tego, co „bezpiecznie stosuje się w kontekście, w którym” oznacza w danym języku programowania.

Podklasowania nie należy mylić z podtytułem. Zasadniczo podtypowanie ustanawia relację is-a, natomiast podklasa ponownie wykorzystuje tylko implementację i ustanawia relację składniową, niekoniecznie relację semantyczną (dziedziczenie nie zapewnia podtypowania behawioralnego).

W celu rozróżnienia tych pojęć, podtypowanie jest również nazywane dziedziczeniem interfejsu , podczas gdy podklasowanie jest nazywane dziedziczeniem implementacji lub dziedziczeniem kodu.

Referencje Dziedziczenie
podtypu

Robert Harvey
źródło
1
Bardzo dobrze powiedziane. Warto wspomnieć w kontekście pytania, że ​​programiści C ++ często używają czystych wirtualnych klas bazowych do komunikowania relacji podtypów z systemem typów. Oczywiście często preferowane są ogólne podejścia programistyczne.
Aluan Haddad
6
„Dokładna semantyka podtypu zależy przede wszystkim od tego, co„ bezpiecznie stosuje się w kontekście, w którym ”oznacza w danym języku programowania”. … A LSP definiuje dość rozsądne pojęcie, co oznacza „bezpiecznie” i mówi nam, jakie ograniczenia te dane szczegółowe muszą spełnić, aby umożliwić tę szczególną formę „bezpieczeństwa”.
Jörg W Mittag,
Jeszcze jedna próbka na stosie: jeśli dobrze zrozumiałem, w C ++ publicdziedziczenie wprowadza podtyp, a privatedziedziczenie wprowadza podklasę.
Quentin
@ Publiczne dziedziczenie @Quentin jest zarówno podtypem, jak i podklasą, ale prywatne jest jeszcze podklasą, ale nie podtypem. Można mieć Subtyping bez instacji ze struktur, takich jak interfejsy Java
eques
27

Typu , w kontekście, że mówimy o tu, jest w istocie zbiorem gwarancji zachowania. Kontrakt , jeśli będzie. Lub, zapożyczając terminologię od Smalltalk, protokołu .

Klasa jest zestawem metod. Jest to zestaw implementacji zachowań .

Podpisywanie jest sposobem na udoskonalenie protokołu. Podklasowanie jest sposobem ponownego wykorzystania kodu różnicowego, tj. Ponownego użycia kodu poprzez opisanie jedynie różnicy w zachowaniu.

Jeśli korzystałeś z Java lub C♯, być może spotkałeś się z radą, że wszystkie typy powinny być interfacetypami. W rzeczywistości, jeśli czytasz William Cook na zrozumieniu danych abstrakcji, Revisited , to może wiedzieć, że aby zrobić OO w tych języków, to musi tylko użycie interfaces jako typy. (Ciekawostka: Java szopowała interfacebezpośrednio z protokołów Objective-C, które z kolei są pobierane bezpośrednio z Smalltalk.)

Teraz, jeśli zastosujemy się do tej porady kodowania do jej logicznej konkluzji i wyobrażymy sobie wersję Java, w której tylko interface s są typami, a klasy i prymitywy nie, wówczas interfacedziedziczenie po drugim spowoduje utworzenie relacji podtypu, podczas gdy classdziedziczenie po drugiej spowoduje być tylko do różnicowego ponownego wykorzystania kodu przez super.

O ile mi wiadomo, nie ma w głównym nurcie języków o typie statycznym, które rozróżniałyby ściśle między dziedziczeniem kodu (dziedziczenie implementacji / podklasowanie) a dziedziczeniem umów (subtyping). W Javie i C♯ dziedziczenie interfejsu jest czystym podtytułem (a przynajmniej tak było, dopóki nie wprowadzono domyślnych metod w Javie 8 i prawdopodobnie również C) 8), ale dziedziczenie klas jest również podrzędne, a także dziedziczenie implementacji. Pamiętam, jak czytałem o eksperymentalnym statycznie zorientowanym obiektowym dialekcie LISP, który ściśle rozróżniał między mixinami (które zawierają zachowanie), strukturami (które zawierają stan), interfejsami (które opisujązachowanie) i klasy (które składają się z zero lub więcej struktur z jedną lub więcej mixinami i są zgodne z jednym lub więcej interfejsami). Można tworzyć instancje tylko klas, a tylko typy mogą być używane jako typy.

W dynamicznie pisanym języku OO, takim jak Python, Ruby, ECMAScript lub Smalltalk, ogólnie myślimy o typie obiektu jako zestawie protokołów, z którymi jest on zgodny. Zwróć uwagę na liczbę mnogą: obiekt może mieć wiele typów, a nie mówię tylko o tym, że każdy obiekt typu Stringjest także obiektem typu Object. (BTW: zwróć uwagę, jak używałem nazw klas do mówienia o typach? Jakie to głupie z mojej strony!) Obiekt może implementować wiele protokołów. Na przykład w Ruby Arraysmożna dołączyć, można je indeksować, iterować i porównywać. To cztery różne protokoły, które wdrażają!

Teraz Ruby nie ma typów. Ale społeczność Ruby ma typy! Istnieją jednak tylko w głowach programistów. I w dokumentacji. Na przykład każdy obiekt, który reaguje na metodę wywoływaną eachprzez poddawanie swoich elementów jeden po drugim, jest uważany za obiekt policzalny . I jest tak zwana mixin, Enumerablektóra zależy od tego protokołu. Dlatego, jeśli obiekt ma prawidłowy typ (która istnieje tylko w głowie programisty), to wolno mieszać w (dziedziczą z) Enumerablewstawek, a także uzyskać wszystkie rodzaje metod chłodnych za darmo, jak map, reduce, filtera więc na.

Podobnie, jeśli obiekt odpowiada <=>, to jest uważany za wdrożenie porównywalny protokół i można go mieszać w Comparablewstawek i inne rzeczy, jak <, <=, >, <=, ==, between?, i clampza darmo. Może jednak również implementować wszystkie te metody i nie dziedziczyć Comparablew ogóle, i nadal byłoby uważane za porównywalne .

Dobrym przykładem jest StringIObiblioteka, która zasadniczo fałszuje strumienie we / wy ciągami. Implementuje wszystkie te same metody co IOklasa, ale nie ma między nimi relacji dziedziczenia. Niemniej jednak StringIOmożna używać wszędzie i IOmożna go używać. Jest to bardzo przydatne w testach jednostkowych, gdzie można zastąpić plik lub stdinz StringIObez konieczności dokonywania dalszych zmian w programie. Ponieważ są StringIOzgodne z tym samym protokołem co IO, oba są tego samego typu, mimo że są różnymi klasami i nie dzielą żadnych relacji (poza trywialnymi, które obaj rozciągają Objectw pewnym momencie).

Jörg W Mittag
źródło
Pomocne może być, jeśli języki pozwolą programom jednocześnie zadeklarować typ klasy i interfejs, dla których ta klasa jest implementacją, a także zezwolić implementacjom na określenie „konstruktorów” (które łączyłyby konstruktory klas określonych przez interfejs). W przypadku typów obiektów, dla których odniesienia byłyby udostępniane publicznie, preferowanym wzorcem byłoby, aby typ klasy był używany tylko podczas tworzenia klas pochodnych; większość odniesień powinna być typu interfejsu. Możliwość określenia konstruktorów interfejsu byłaby pomocna w sytuacjach, w których ...
supercat,
... np. kod potrzebuje kolekcji, która pozwoli na odczyt określonego zestawu wartości przez indeks, ale tak naprawdę nie obchodzi go, jaki to typ. Chociaż istnieją uzasadnione powody, by uznawać klasy i interfejsy za odrębne rodzaje tekstu, istnieje wiele sytuacji, w których powinni oni być w stanie współpracować ściślej niż pozwala na to obecnie język.
supercat
Czy masz odniesienie lub jakieś słowa kluczowe, że mógłbym wyszukać więcej informacji na temat wspomnianego przez ciebie eksperymentalnego dialektu LISP, który formalnie różnicuje miksy, struktury, interfejsy i klasy?
tel
@tel: Nie, przepraszam. To było prawdopodobnie około 15-20 lat temu, a wtedy moje zainteresowania były wszędzie. Nie mogłem zacząć mówić, czego szukałem, kiedy natknąłem się na to.
Jörg W Mittag
Awww. To był najciekawszy szczegół we wszystkich tych odpowiedziach. Fakt, że formalne rozdzielenie tych pojęć jest rzeczywiście możliwe w ramach implementacji języka, naprawdę pomogło mi skrystalizować rozróżnienie klas / typów. Tak czy inaczej, chyba pójdę poszukać tego LISP-a. Czy zdarza ci się pamiętać, że czytasz o tym w artykule / książce z czasopisma, czy po prostu słyszysz o tym w rozmowie?
tel
2

Być może najpierw warto rozróżnić typ od klasy, a następnie zgłębić różnicę między podtypami a podklasami.

W pozostałej części tej odpowiedzi zakładam, że omawiane typy są typami statycznymi (ponieważ podtyp zwykle pojawia się w kontekście statycznym).

Opracuję pseudokod zabawkowy, który pomoże zilustrować różnicę między typem a klasą, ponieważ większość języków zbiega je przynajmniej częściowo (nie bez powodu).

Zacznijmy od rodzaju. Typ to etykieta wyrażenia w kodzie. Wartość tej etykiety i to, czy jest ona spójna (dla niektórych definicji spójnych specyficznych dla systemu) ze wszystkimi pozostałymi wartościami etykiet, może być określona przez program zewnętrzny (sprawdzanie typu) bez uruchamiania programu. To sprawia, że ​​te etykiety są wyjątkowe i zasługują na własne imię.

W naszym języku zabawek możemy pozwolić na tworzenie takich etykiet.

declare type Int
declare type String

Następnie moglibyśmy oznaczyć różne wartości jako tego typu.

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

Dzięki tym instrukcjom nasz typechecker może teraz odrzucić takie instrukcje jak

0 is of type String

jeśli jednym z wymagań naszego systemu typów jest to, że każde wyrażenie ma unikalny typ.

Odłóżmy na razie na bok, jak jest to niezgrabne i jak będziesz miał problemy z przypisaniem nieskończonej liczby typów wyrażeń. Możemy do niego wrócić później.

Z drugiej strony klasa jest zbiorem metod i pól, które są zgrupowane razem (potencjalnie z modyfikatorami dostępu, takimi jak prywatny lub publiczny).

class StringClass:
  defMethod concatenate(otherString): ...
  defField size: ...

Wystąpienie tej klasy umożliwia tworzenie lub używanie wcześniej istniejących definicji tych metod i pól.

Możemy powiązać klasę z typem, tak aby każda instancja klasy była automatycznie oznaczana tym typem.

associate StringClass with String

Ale nie każdy typ musi mieć powiązaną klasę.

# Hmm... Doesn't look like there's a class for Int

Możliwe jest również, że w naszym języku zabawek nie każda klasa ma typ, szczególnie jeśli nie wszystkie nasze wyrażenia mają typy. Trochę trudniejsze (ale nie niemożliwe) jest wyobrażenie sobie, jak wyglądałyby reguły spójności systemu typów, gdyby niektóre wyrażenia zawierały typy, a niektóre nie.

Ponadto w naszym języku zabawek skojarzenia te nie muszą być wyjątkowe. Możemy powiązać dwie klasy z tym samym typem.

associate MyCustomStringClass with String

Pamiętaj, że nasz sprawdzanie typów nie wymaga śledzenia wartości wyrażenia (w większości przypadków nie jest to niemożliwe lub jest niemożliwe). Wszystko, co wie, to etykiety, które powiedziałeś. Dla przypomnienia, sprawdzanie typów było w stanie odrzucić instrukcję tylko z 0 is of type Stringpowodu naszej sztucznie stworzonej reguły typu, że wyrażenia muszą mieć unikalne typy, a my już oznaczyliśmy wyrażenie jako 0coś innego. Nie miał żadnej specjalnej wiedzy na temat wartości 0.

A co z podsieciami? Cóż, podtyp jest nazwą wspólnej reguły sprawdzania typu, która rozluźnia inne reguły, które możesz mieć. Mianowicie, jeśli A is subtype of Bwtedy wszędzie twój typechecker wymaga etykiety B, akceptuje również A.

Na przykład możemy wykonać następujące czynności dla naszych numerów zamiast tego, co mieliśmy wcześniej.

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

Podklasowanie jest skrótem do deklarowania nowej klasy, która pozwala na ponowne użycie wcześniej zadeklarowanych metod i pól.

class ExtendedStringClass is subclass of StringClass:
  # We get concatenate and size for free!
  def addQuestionMark: ...

Nie musimy kojarzyć instancji ExtendedStringClassz, Stringjak to zrobiliśmy, StringClassponieważ w końcu jest to zupełnie nowa klasa, po prostu nie musieliśmy tyle pisać. Pozwoliłoby nam to podać ExtendedStringClasstyp, który jest niezgodny z Stringpunktu widzenia sprawdzającego typ.

Podobnie moglibyśmy postanowić stworzyć zupełnie nową klasę NewClassi gotowe

associate NewClass with String

Teraz każde wystąpienie StringClassmoże zostać zastąpione z NewClasspunktu widzenia sprawdzającego typ.

Teoretycznie podtypy i podklasy są zupełnie różnymi rzeczami. Ale żaden język, który znam, nie ma typów i klas, nie robi rzeczy w ten sposób. Zacznijmy analizować nasz język i wyjaśnić uzasadnienie niektórych naszych decyzji.

Po pierwsze, chociaż teoretycznie zupełnie innym klasom można nadać ten sam typ lub klasie można nadać ten sam typ co wartości, które nie są instancjami żadnej klasy, poważnie ogranicza to przydatność sprawdzania typów. Sprawdzanie typów jest skutecznie okradane ze zdolności sprawdzania, czy metoda lub pole, które wywołujesz w wyrażeniu, faktycznie istnieje dla tej wartości, co jest prawdopodobnie sprawdzeniem, które chciałbyś, jeśli masz problem z graniem razem z typechecker. W końcu kto wie, jaka jest wartość pod tą Stringetykietą; może to być coś, co nie ma na przykład żadnej concatenatemetody!

Ok, zastanówmy się, że każda klasa automatycznie generuje nowy typ o tej samej nazwie co associateinstancje tej klasy z tym typem. Pozwala nam to pozbyć associatesię różnych nazw między StringClassi String.

Z tego samego powodu prawdopodobnie chcemy automatycznie ustanowić relację podtypu między typami dwóch klas, z których jedna jest podklasą innej. Po zagwarantowaniu, że cała podklasa ma wszystkie metody i pola, które posiada klasa nadrzędna, ale odwrotna sytuacja nie jest prawdą. Dlatego chociaż podklasa może przejść w dowolnym momencie, gdy potrzebujesz typu klasy nadrzędnej, typ klasy nadrzędnej powinien zostać odrzucony, jeśli potrzebujesz typu podklasy.

Jeśli połączysz to z zastrzeżeniem, że wszystkie wartości zdefiniowane przez użytkownika muszą być instancjami klasy, możesz uzyskać is subclass ofpodwójne obciążenie i się go pozbyć is subtype of.

I to prowadzi nas do cech, które łączy większość popularnych statycznie pisanych języków OO. Istnieje zestaw „prymitywnych” typów (np int, floatetc.), które nie są związane z żadną klasą i nie są definiowane przez użytkownika. Następnie masz wszystkie klasy zdefiniowane przez użytkownika, które automatycznie mają typy o tej samej nazwie i identyfikują podklasy za pomocą podtypów.

Ostatnia uwaga, którą zrobię, dotyczy niezręczności deklarowania typów niezależnie od wartości. Większość języków łączy w sobie tworzenie tych dwóch, więc deklaracja typu jest również deklaracją do generowania zupełnie nowych wartości, które są automatycznie oznaczane tym typem. Na przykład deklaracja klasy zazwyczaj tworzy zarówno typ, jak i sposób tworzenia wartości tego typu. Pozbywa się to trochę niezgrabności, a w obecności konstruktorów pozwala także tworzyć nieskończenie wiele wartości z jednym typem za jednym pociągnięciem.

badcook
źródło