La réponse la mieux notée à cette question sur le principe de substitution de Liskov s'efforce de distinguer les termes sous - type et sous - classe . Cela montre également que certaines langues confondent les deux, alors que d'autres ne le font pas.
Pour les langages orientés objet que je connais le mieux (Python, C ++), "type" et "classe" sont des concepts synonymes. En termes de C ++, qu'est-ce que cela signifierait d'avoir une distinction entre sous-type et sous-classe? Disons, par exemple, qu’il Foo
s’agit d’une sous-classe, mais pas d’un sous-type FooBase
. Si foo
est une instance de Foo
, cette ligne:
FooBase* fbPoint = &foo;
ne plus être valide?
Réponses:
Le sous-typage est une forme de polymorphisme de type dans laquelle un sous-type est un type de données lié à un autre type de données (le supertype) par une notion de substituabilité, ce qui signifie que des éléments de programme, généralement des sous-programmes ou des fonctions, écrits pour agir sur des éléments du supertype peuvent également opérer sur des éléments du sous-type.
Si
S
est un sous-type deT
, la relation de sous-typage est souvent écriteS <: T
, pour signifier que tout terme de typeS
peut être utilisé en toute sécurité dans un contexte où un terme de typeT
est attendu. La sémantique précise du sous-typage dépend de manière cruciale de ce que "est utilisé en toute sécurité dans un contexte où" signifie dans un langage de programmation donné.Le sous-classement ne doit pas être confondu avec le sous-typage. En général, le sous-typage établit une relation is-a, tandis que le sous-classement ne fait que réutiliser une implémentation et établit une relation syntaxique, pas nécessairement une relation sémantique (l'héritage n'assure pas le sous-typage comportemental).
Pour distinguer ces concepts, le sous-typage est également appelé héritage d'interface , tandis que le sous-classement est appelé héritage d'implémentation ou héritage de code.
Références
Sous-typage
Héritage
la source
public
héritage introduit un sous-type, tandis que l'private
héritage introduit une sous-classe.Un type , dans le contexte dont nous parlons ici, est essentiellement un ensemble de garanties comportementales. Un contrat , si vous voulez. Ou, empruntant la terminologie de Smalltalk, un protocole .
Une classe est un ensemble de méthodes. C'est un ensemble d' implémentations de comportement .
Le sous-typage est un moyen d'affiner le protocole. Le sous - classement est un moyen de réutiliser du code différentiel, c'est-à-dire de réutiliser du code en décrivant uniquement la différence de comportement.
Si vous avez utilisé Java ou C♯, vous avez peut-être trouvé le conseil selon lequel tous les types devraient être des
interface
types. En fait, si vous lisez On Understanding Abstraction, Revisited de William Cook , vous savez peut-être que, pour exécuter OO dans ces langues, vous ne devez utiliser queinterface
s comme types. (En outre, fait amusant: Java a été crééinterface
directement à partir des protocoles d’Objective-C, qui sont eux-mêmes extraits directement de Smalltalk.)Maintenant, si nous suivons que le codage des conseils à sa conclusion logique et d' imaginer une version de Java, où seuls
interface
s sont les types et les classes et les primitives ne sont pas, alors oninterface
héritant d' une autre va créer une relation de sous - typage, alors qu'unclass
héritant d' une autre volonté être simplement pour la réutilisation de code différentiel viasuper
.Autant que je sache, il n’existe pas de langages classiques à typage statique qui établissent une distinction stricte entre le code hérité (héritage d’implémentation / sous-classement) et les contrats hérités (sous-typage). En Java et en C♯, l'héritage d'interface est un sous-type pur (ou du moins, jusqu'à l'introduction des méthodes par défaut dans Java 8 et probablement aussi en C 8), mais l'héritage de classe est également un sous-type et l'héritage d'implémentation. Je me souviens d’avoir lu un article sur un dialecte LISP expérimental à typage statique, orienté objet, qui distinguait strictement les mixins (qui contiennent le comportement), les structs (qui contiennent l’état), les interfaces (qui décriventcomportement) et les classes (qui composent zéro ou plusieurs structures avec un ou plusieurs mixins et se conforment à une ou plusieurs interfaces). Seules les classes peuvent être instanciées et seules les interfaces peuvent être utilisées en tant que types.
Dans un langage OO à typage dynamique tel que Python, Ruby, ECMAScript ou Smalltalk, nous pensons généralement que le ou les types d'objet sont l'ensemble des protocoles auxquels il se conforme. Notez le pluriel: un objet peut avoir plusieurs types, et je ne parle pas seulement du fait que chaque objet de type
String
est également un objet de typeObject
. (BTW: notez comment j'ai utilisé les noms de classe pour parler de types? Comment stupide de moi!) Un objet peut implémenter plusieurs protocoles. Par exemple, dans Ruby, vousArrays
pouvez les ajouter, les indexer, les itérer et les comparer. C'est quatre protocoles différents qu'ils implémentent!Maintenant, Ruby n'a pas de types. Mais la communauté Ruby a des types! Ils n'existent que dans la tête des programmeurs. Et dans la documentation. Par exemple, tout objet qui répond à une méthode appelée
each
en donnant ses éléments un par un est considéré comme un objet énumérable . Et il y a un mixin appeléEnumerable
qui dépend de ce protocole. Donc, si votre objet a le bon genre (qui existe seulement dans la tête du programmeur), il est permis de mélanger (Hériter) leEnumerable
mixin, et bien obtenir toutes sortes de méthodes fraîches gratuitement, commemap
,reduce
,filter
et ainsi sur.De même, si un objet répond à
<=>
, il est alors considéré à mettre en œuvre le comparable protocole, et il peut mélanger dans leComparable
mixin et obtenir des choses comme<
,<=
,>
,<=
,==
,between?
etclamp
gratuitement. Cependant, il peut également mettre en œuvre toutes ces méthodes lui-même, sans en hériter duComparable
tout, et il serait toujours considéré comme comparable .Un bon exemple est la
StringIO
bibliothèque, qui simule essentiellement les flux d’E / S avec des chaînes. Il implémente toutes les mêmes méthodes que laIO
classe, mais il n'y a pas de relation d'héritage entre les deux. Néanmoins, unStringIO
peut être utilisé partout etIO
peut être utilisé. Ceci est très utile dans les tests unitaires, où vous pouvez remplacer un fichier oustdin
par unStringIO
sans avoir à apporter d'autres modifications à votre programme. Comme ils seStringIO
conforment au même protocoleIO
, ils sont du même type, même s’ils appartiennent à des classes différentes, et ne partagent aucune relation (autre que la simple trivial qu’ils s’étendentObject
à un moment donné).la source
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.
Ensuite, nous pourrions étiqueter diverses valeurs comme étant de ce type.
Avec ces déclarations, notre vérificateur de typage peut maintenant rejeter des déclarations telles que
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).
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.
Mais tous les types n'ont pas besoin d'avoir une classe associée.
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.
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'expression0
. Il n'avait aucune connaissance particulière de la valeur de0
.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 étiquetteB
, il acceptera également unA
.Par exemple, nous pourrions faire ce qui suit pour nos nombres au lieu de ce que nous avions auparavant.
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.
Nous n'avons pas à associer des instances de
ExtendedStringClass
withString
comme nous l'avons faitStringClass
car, après tout, c'est une toute nouvelle classe, nous n'avons pas eu à écrire autant. Cela nous permettrait de donnerExtendedStringClass
un type incompatible avecString
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 faireDésormais, chaque instance de
StringClass
peut être remplacée par celleNewClass
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, uneconcatenate
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 desassociate
noms différents entreStringClass
etString
.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ébarrasseris 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.
la source