È forse utile innanzitutto distinguere tra un tipo e una classe e quindi immergersi nella differenza tra sottotipo e sottoclasse.
Per il resto di questa risposta suppongo che i tipi in discussione siano tipi statici (dal momento che il sottotipo di solito si presenta in un contesto statico).
Svilupperò uno pseudocodice giocattolo per aiutare a illustrare la differenza tra un tipo e una classe perché la maggior parte delle lingue li confonde almeno in parte (per una buona ragione che toccherò brevemente).
Cominciamo con un tipo. Un tipo è un'etichetta per un'espressione nel tuo codice. Il valore di questa etichetta e se è coerente (per alcuni tipi di definizione specifica del sistema di coerenza) con il valore di tutte le altre etichette può essere determinato da un programma esterno (un typechecker) senza eseguire il programma. Questo è ciò che rende queste etichette speciali e meritevoli del proprio nome.
Nel nostro linguaggio dei giocattoli potremmo consentire la creazione di etichette in questo modo.
declare type Int
declare type String
Quindi potremmo etichettare vari valori come di questo tipo.
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
...
Con queste affermazioni il nostro typechecker può ora rifiutare dichiarazioni come
0 is of type String
se uno dei requisiti del nostro sistema di tipi è che ogni espressione ha un tipo unico.
Lasciamo da parte per ora quanto sia goffo e come avrete problemi ad assegnare un numero infinito di tipi di espressioni. Possiamo tornarci più tardi.
Una classe d'altra parte è una raccolta di metodi e campi che sono raggruppati insieme (potenzialmente con modificatori di accesso come privati o pubblici).
class StringClass:
defMethod concatenate(otherString): ...
defField size: ...
Un'istanza di questa classe ha la possibilità di creare o utilizzare definizioni preesistenti di questi metodi e campi.
Potremmo scegliere di associare una classe a un tipo in modo tale che ogni istanza di una classe sia automaticamente etichettata con quel tipo.
associate StringClass with String
Ma non tutti i tipi devono avere una classe associata.
# Hmm... Doesn't look like there's a class for Int
È anche possibile che nel nostro linguaggio dei giocattoli non ogni classe abbia un tipo, specialmente se non tutte le nostre espressioni hanno tipi. È un po 'più complicato (ma non impossibile) immaginare che tipo di regole di coerenza del sistema apparirebbero se alcune espressioni avessero dei tipi e altre no.
Inoltre, nel nostro linguaggio dei giocattoli, queste associazioni non devono essere uniche. Potremmo associare due classi con lo stesso tipo.
associate MyCustomStringClass with String
Ora tieni presente che non è necessario che il nostro typechecker tenga traccia del valore di un'espressione (e nella maggior parte dei casi non sarà o è impossibile farlo). Tutto ciò che sa sono le etichette che gli hai detto. Come promemoria in precedenza, il typechecker era in grado di rifiutare l'affermazione solo a 0 is of type String
causa della nostra regola del tipo creata artificialmente che le espressioni devono avere tipi univoci e avevamo già etichettato l'espressione 0
qualcos'altro. Non aveva alcuna conoscenza speciale del valore di 0
.
E che dire del sottotipo? Bene il sottotipo è un nome per una regola comune nel controllo dei caratteri che rilassa le altre regole che potresti avere. Vale a dire se A is subtype of B
quindi ovunque il tuo typechecker richiede un'etichetta di B
, accetterà anche un A
.
Ad esempio, potremmo fare quanto segue per i nostri numeri invece di quello che avevamo in precedenza.
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
...
La sottoclasse è una scorciatoia per dichiarare una nuova classe che consente di riutilizzare metodi e campi precedentemente dichiarati.
class ExtendedStringClass is subclass of StringClass:
# We get concatenate and size for free!
def addQuestionMark: ...
Non abbiamo alle istanze associati di ExtendedStringClass
con String
come abbiamo fatto con StringClass
poiché, dopo tutto è una classe del tutto nuova, abbiamo semplicemente non devono scrivere tanto. Ciò ci consentirebbe di fornire ExtendedStringClass
un tipo incompatibile String
dal punto di vista del tipechecker.
Allo stesso modo avremmo potuto decidere di creare una classe completamente nuova NewClass
e fatto
associate NewClass with String
Ora ogni istanza di StringClass
può essere sostituita NewClass
dal punto di vista del typechecker.
Quindi in teoria il sottotipo e la sottoclasse sono cose completamente diverse. Ma nessuna lingua che conosco ha tipi e classi in realtà fa le cose in questo modo. Iniziamo a ridurre la nostra lingua e spieghiamo la logica alla base di alcune delle nostre decisioni.
Prima di tutto, anche se in teoria a classi completamente diverse potrebbe essere assegnato lo stesso tipo o a una classe potrebbe essere assegnato lo stesso tipo di valori che non sono istanze di alcuna classe, ciò ostacola gravemente l'utilità del typechecker. Il typechecker è effettivamente privato della possibilità di verificare se il metodo o il campo che stai chiamando all'interno di un'espressione esiste effettivamente su quel valore, che è probabilmente un controllo che ti piacerebbe se stai andando a giocare con un coontrollore dei tipo. Dopotutto, chissà quale sia il valore reale sotto String
quell'etichetta; potrebbe essere qualcosa che non ha affatto, ad esempio, un concatenate
metodo!
Ok, quindi stipuliamo che ogni classe genera automaticamente un nuovo tipo con lo stesso nome della classe e delle associate
istanze di quel tipo. Questo ci consente di sbarazzarci di associate
così come i diversi nomi tra StringClass
e String
.
Per lo stesso motivo, probabilmente vogliamo stabilire automaticamente una relazione di sottotipo tra i tipi di due classi in cui una è una sottoclasse di un'altra. Dopo tutto, la sottoclasse è garantita per avere tutti i metodi e campi della classe genitore, ma non è vero il contrario. Pertanto, mentre la sottoclasse può passare ogni volta che è necessario un tipo di classe genitore, il tipo di classe genitore deve essere rifiutato se è necessario il tipo di sottoclasse.
Se si combina questo con la stipula secondo cui tutti i valori definiti dall'utente devono essere istanze di una classe, è possibile avere is subclass of
doppio dovere e liberarsene is subtype of
.
E questo ci porta alle caratteristiche che condividono la maggior parte delle lingue popolari OO tipicamente statiche. Esistono un insieme di tipi "primitivi" (ad es int
. float
, Ecc.) Che non sono associati a nessuna classe e non sono definiti dall'utente. Quindi hai tutte le classi definite dall'utente che hanno automaticamente tipi con lo stesso nome e identificano la sottoclasse con il sottotipo.
La nota finale che prenderò in considerazione è la confusione di dichiarare i tipi separatamente dai valori. La maggior parte delle lingue confonde la creazione delle due, in modo che una dichiarazione di tipo sia anche una dichiarazione per la generazione di valori completamente nuovi che vengono automaticamente etichettati con quel tipo. Ad esempio, una dichiarazione di classe in genere crea sia il tipo che un modo per creare un'istanza di valori di quel tipo. Questo elimina parte della confusione e, in presenza di costruttori, consente anche di creare un'etichetta infinita di valori con un tipo in un colpo.