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 String
powodu naszej sztucznie stworzonej reguły typu, że wyrażenia muszą mieć unikalne typy, a my już oznaczyliśmy wyrażenie jako 0
coś 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 B
wtedy 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 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 z String
punktu widzenia sprawdzającego typ.
Podobnie moglibyśmy postanowić stworzyć zupełnie nową klasę NewClass
i gotowe
associate NewClass with String
Teraz każde wystąpienie StringClass
może zostać zastąpione z NewClass
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 żadnej concatenate
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ędzy StringClass
i 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 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.