Il est peut-être utile d’abord de faire la distinction entre un type et une classe, puis de plonger dans la différence entre le sous-typage et le sous-classement.
Pour le reste de cette réponse, je vais supposer que les types en discussion sont des types statiques (car le sous-typage apparaît généralement dans un contexte statique).
Je vais développer un pseudocode jouet pour illustrer la différence entre un type et une classe, car la plupart des langues les confondent au moins en partie (pour la bonne raison que je vais aborder brièvement).
Commençons par un type. Un type est une étiquette pour une expression dans votre code. La valeur de cette étiquette et son éventuelle cohérence (pour une définition de cohérence spécifique à un système de type) avec la valeur de toutes les autres étiquettes peuvent être déterminées par un programme externe (un vérificateur de type) sans exécuter votre programme. C'est ce qui rend ces étiquettes spéciales et méritant leur propre nom.
Dans notre langage des jouets, nous pourrions permettre la création d'étiquettes comme celle-ci.
declare type Int
declare type String
Ensuite, nous pourrions étiqueter diverses valeurs comme étant de ce type.
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
...
Avec ces déclarations, notre vérificateur de typage peut maintenant rejeter des déclarations telles que
0 is of type String
si l’une des exigences de notre système de types est que chaque expression ait un type unique.
Laissons de côté pour l'instant à quel point c'est maladroit et comment vous allez avoir des problèmes pour assigner un nombre infini de types d'expressions. Nous pouvons y revenir plus tard.
Une classe, par contre, est un ensemble de méthodes et de champs regroupés (éventuellement avec des modificateurs d’accès tels que privé ou public).
class StringClass:
defMethod concatenate(otherString): ...
defField size: ...
Une instance de cette classe obtient la possibilité de créer ou d'utiliser des définitions préexistantes de ces méthodes et champs.
Nous pourrions choisir d'associer une classe à un type de sorte que chaque instance d'une classe soit automatiquement étiquetée avec ce type.
associate StringClass with String
Mais tous les types n'ont pas besoin d'avoir une classe associée.
# Hmm... Doesn't look like there's a class for Int
Il est également concevable que, dans notre langage de jouet, toutes les classes n'aient pas un type, surtout si toutes nos expressions n'ont pas de types. Il est un peu plus délicat (mais pas impossible) d'imaginer à quoi ressembleraient les règles de cohérence des systèmes de types si certaines expressions avaient des types et d'autres pas.
De plus, dans notre langage des jouets, ces associations ne doivent pas nécessairement être uniques. Nous pourrions associer deux classes du même type.
associate MyCustomStringClass with String
Maintenant, gardez à l'esprit qu'il n'y a aucune obligation pour notre vérificateur de typage de suivre la valeur d'une expression (et dans la plupart des cas, il est impossible ou impossible de le faire). Tout ce qu'il sait, ce sont les étiquettes que vous lui avez dites. Pour rappel, le vérificateur de type ne pouvait rejeter la déclaration que 0 is of type String
parce que notre règle de type créée artificiellement stipulait que les expressions devaient avoir des types uniques et que nous avions déjà étiqueté l'expression 0
. Il n'avait aucune connaissance particulière de la valeur de 0
.
Alors qu'en est-il du sous-typage? Le sous-typage est le nom d’une règle commune en vérification de type qui assouplit les autres règles que vous pourriez avoir. C'est-à-dire que si, A is subtype of B
partout, votre vérificateur de type exige une étiquette B
, il acceptera également un A
.
Par exemple, nous pourrions faire ce qui suit pour nos nombres au lieu de ce que nous avions auparavant.
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
...
Le sous-classement est un raccourci pour déclarer une nouvelle classe qui vous permet de réutiliser des méthodes et des champs précédemment déclarés.
class ExtendedStringClass is subclass of StringClass:
# We get concatenate and size for free!
def addQuestionMark: ...
Nous n'avons pas à associer des instances de ExtendedStringClass
with String
comme nous l'avons fait StringClass
car, après tout, c'est une toute nouvelle classe, nous n'avons pas eu à écrire autant. Cela nous permettrait de donner ExtendedStringClass
un type incompatible avec String
le point de vue du vérificateur de type.
De même, nous aurions pu décider de faire une toute nouvelle classe NewClass
et faire
associate NewClass with String
Désormais, chaque instance de StringClass
peut être remplacée par celle NewClass
du typechecker.
Donc, en théorie, le sous-typage et le sous-classement sont des choses complètement différentes. Mais aucune langue que je connaisse qui ait des types et des classes ne fait réellement les choses de cette façon. Commençons par réduire notre langage et à expliquer les raisons de certaines de nos décisions.
Tout d'abord, même si, en théorie, des classes complètement différentes pourraient se voir attribuer le même type ou à une classe du même type que des valeurs qui ne sont des instances d'aucune classe, cela nuit gravement à l'utilité du vérificateur de type. Le vérificateur de typage est effectivement privé de la possibilité de vérifier si la méthode ou le champ que vous appelez dans une expression existe réellement sur cette valeur, ce qui est probablement une vérification que vous voudriez si vous avez la peine de jouer avec une typechecker. Après tout, qui sait quelle est la valeur sous cette String
étiquette? ce pourrait être quelque chose qui n'a pas, par exemple, une concatenate
méthode du tout!
Bon, stipulons que chaque classe génère automatiquement un nouveau type du même nom que cette classe et associate
s instances de ce type. Cela nous permet de nous débarrasser des associate
noms différents entre StringClass
et String
.
Pour la même raison, nous souhaitons probablement établir automatiquement une relation de sous-type entre les types de deux classes, l'une étant une sous-classe d'une autre. Après que toute la sous-classe soit assurée d'avoir toutes les méthodes et tous les champs de la classe parente, mais l'inverse n'est pas vrai. Par conséquent, bien que la sous-classe puisse passer à tout moment si vous avez besoin d'un type de la classe parent, le type de la classe parent doit être rejeté si vous avez besoin du type de la sous-classe.
Si vous combinez cela avec la stipulation que toutes les valeurs définies par l'utilisateur doivent être des instances d'une classe, vous pouvez alors avoir is subclass of
le double devoir et vous en débarrasser is subtype of
.
Et cela nous amène aux caractéristiques que partagent la plupart des langages OO statiquement typés populaires. Il existe un ensemble de types « primitifs » (par exemple int
, float
, etc.) qui ne sont pas associés à une classe et ne sont pas définis par l' utilisateur. Ensuite, vous avez toutes les classes définies par l'utilisateur qui ont automatiquement des types du même nom et identifient les sous-classes avec les sous-types.
La dernière note que je vais faire concerne la confusion qui consiste à déclarer des types séparément des valeurs. La plupart des langues confondent la création des deux, de sorte qu'une déclaration de type est également une déclaration permettant de générer des valeurs entièrement nouvelles étiquetées automatiquement avec ce type. Par exemple, une déclaration de classe crée généralement le type ainsi qu'un moyen d'instanciation des valeurs de ce type. Cela supprime une partie de la dislocation et, en présence de constructeurs, vous permet également de créer une infinité de valeurs de libellé avec un type d'un trait.