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 Foo
jest to podklasa, ale nie podtyp FooBase
. Jeśli foo
jest instancją Foo
, czy ta linia:
FooBase* fbPoint = &foo;
nie jest już ważny?
Odpowiedzi:
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
S
jest podtypemT
, relacja podtypu jest często zapisywanaS <: T
, co oznacza, że dowolny termin typuS
może być bezpiecznie używany w kontekście, w którymT
oczekiwany 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
źródło
public
dziedziczenie wprowadza podtyp, aprivate
dziedziczenie wprowadza podklasę.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ć
interface
typami. 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życieinterface
s jako typy. (Ciekawostka: Java szopowałainterface
bezpoś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ówczasinterface
dziedziczenie po drugim spowoduje utworzenie relacji podtypu, podczas gdyclass
dziedziczenie po drugiej spowoduje być tylko do różnicowego ponownego wykorzystania kodu przezsuper
.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
String
jest także obiektem typuObject
. (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 RubyArrays
moż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ą
each
przez poddawanie swoich elementów jeden po drugim, jest uważany za obiekt policzalny . I jest tak zwana mixin,Enumerable
któ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)Enumerable
wstawek, a także uzyskać wszystkie rodzaje metod chłodnych za darmo, jakmap
,reduce
,filter
a więc na.Podobnie, jeśli obiekt odpowiada
<=>
, to jest uważany za wdrożenie porównywalny protokół i można go mieszać wComparable
wstawek i inne rzeczy, jak<
,<=
,>
,<=
,==
,between?
, iclamp
za darmo. Może jednak również implementować wszystkie te metody i nie dziedziczyćComparable
w ogóle, i nadal byłoby uważane za porównywalne .Dobrym przykładem jest
StringIO
biblioteka, która zasadniczo fałszuje strumienie we / wy ciągami. Implementuje wszystkie te same metody coIO
klasa, ale nie ma między nimi relacji dziedziczenia. Niemniej jednakStringIO
można używać wszędzie iIO
można go używać. Jest to bardzo przydatne w testach jednostkowych, gdzie można zastąpić plik lubstdin
zStringIO
bez konieczności dokonywania dalszych zmian w programie. Ponieważ sąStringIO
zgodne z tym samym protokołem coIO
, oba są tego samego typu, mimo że są różnymi klasami i nie dzielą żadnych relacji (poza trywialnymi, które obaj rozciągająObject
w pewnym momencie).źródło
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.
Następnie moglibyśmy oznaczyć różne wartości jako tego typu.
Dzięki tym instrukcjom nasz typechecker może teraz odrzucić takie instrukcje jak
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).
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.
Ale nie każdy typ musi mieć powiązaną klasę.
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.
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 String
powodu naszej sztucznie stworzonej reguły typu, że wyrażenia muszą mieć unikalne typy, a my już oznaczyliśmy wyrażenie jako0
coś innego. Nie miał żadnej specjalnej wiedzy na temat wartości0
.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 B
wtedy wszędzie twój typechecker wymaga etykietyB
, 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.
Podklasowanie jest skrótem do deklarowania nowej klasy, która pozwala na ponowne użycie wcześniej zadeklarowanych metod i pól.
Nie musimy kojarzyć instancji
ExtendedStringClass
z,String
jak to zrobiliśmy,StringClass
ponieważ w końcu jest to zupełnie nowa klasa, po prostu nie musieliśmy tyle pisać. Pozwoliłoby nam to podaćExtendedStringClass
typ, który jest niezgodny zString
punktu widzenia sprawdzającego typ.Podobnie moglibyśmy postanowić stworzyć zupełnie nową klasę
NewClass
i gotoweTeraz każde wystąpienie
StringClass
może zostać zastąpione zNewClass
punktu 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ą
String
etykietą; może to być coś, co nie ma na przykład żadnejconcatenate
metody!Ok, zastanówmy się, że każda klasa automatycznie generuje nowy typ o tej samej nazwie co
associate
instancje tej klasy z tym typem. Pozwala nam to pozbyćassociate
się różnych nazw międzyStringClass
iString
.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 of
podwó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
,float
etc.), 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.
źródło