Quelle est la différence entre une sous-classe et un sous-type?

44

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 Foos’agit d’une sous-classe, mais pas d’un sous-type FooBase. Si fooest une instance de Foo, cette ligne:

FooBase* fbPoint = &foo;

ne plus être valide?

tel
la source
6
En fait, en Python, "type" et "classe" sont des concepts distincts . En fait, Python étant typé dynamiquement, « type » est pas un concept tout en Python. Malheureusement, les développeurs Python ne comprennent pas cela et continuent de confondre les deux.
Jörg W Mittag
11
"Type" et "classe" sont également distincts en C ++. "Tableau d'ints" est un type; quelle classe est-ce? "pointeur sur une variable de type int" est un type; quelle classe est-ce? Ces choses ne sont pas une classe, mais ce sont sûrement des types.
Eric Lippert
2
Je me demandais cette chose même après avoir lu cette question et cette réponse.
user369450
4
@JorgWMittag S'il n'y a pas de concept de "type" en python, quelqu'un doit le dire à quiconque écrit la documentation: docs.python.org/3/library/stdtypes.html
Matt
@Matt pour être juste, les types ont été insérés dans la version 3.5, ce qui est assez récent, en particulier selon les normes de ce que je suis autorisé à utiliser en production.
Jared Smith

Réponses:

53

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 Sest un sous-type de T, la relation de sous-typage est souvent écrite S <: T, pour signifier que tout terme de type Speut être utilisé en toute sécurité dans un contexte où un terme de type Test 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

Robert Harvey
la source
1
Très bien dit. Il convient de mentionner, dans le contexte de la question, que les programmeurs C ++ utilisent souvent des classes de base virtuelles pures pour communiquer des relations de sous-typage au système de types. Les approches de programmation génériques sont souvent préférées bien sûr.
Aluan Haddad
6
"La sémantique précise du sous-typage dépend essentiellement des détails de" ce qui est utilisé en toute sécurité dans un contexte où "signifie dans un langage de programmation donné". … Et le LSP définit une idée assez raisonnable de ce que signifie "en toute sécurité" et nous indique les contraintes que ces particularités doivent satisfaire pour permettre cette forme particulière de "sécurité".
Jörg W Mittag
Encore un exemple sur la pile: si j'ai bien compris, en C ++, l' publichéritage introduit un sous-type, tandis que l' privatehéritage introduit une sous-classe.
Quentin
L'héritage public de @Quentin est à la fois un sous-type et une sous-classe, mais privé n'est qu'une sous-classe, mais pas un sous-type. Vous pouvez avoir des sous-typages sans sous-classer avec des structures comme les interfaces Java
eques
27

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 interfacetypes. 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 que interfaces comme types. (En outre, fait amusant: Java a été créé interfacedirectement à 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 on interfacehéritant d' une autre va créer une relation de sous - typage, alors qu'un classhéritant d' une autre volonté être simplement pour la réutilisation de code différentiel via super.

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 Stringest également un objet de type Object. (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, vous Arrayspouvez 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 eachen donnant ses éléments un par un est considéré comme un objet énumérable . Et il y a un mixin appelé Enumerablequi 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) le Enumerablemixin, et bien obtenir toutes sortes de méthodes fraîches gratuitement, comme map, reduce, filteret 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 le Comparablemixin et obtenir des choses comme <, <=, >, <=, ==, between?et clampgratuitement. Cependant, il peut également mettre en œuvre toutes ces méthodes lui-même, sans en hériter du Comparabletout, et il serait toujours considéré comme comparable .

Un bon exemple est la StringIObibliothèque, qui simule essentiellement les flux d’E / S avec des chaînes. Il implémente toutes les mêmes méthodes que la IOclasse, mais il n'y a pas de relation d'héritage entre les deux. Néanmoins, un StringIOpeut être utilisé partout et IOpeut être utilisé. Ceci est très utile dans les tests unitaires, où vous pouvez remplacer un fichier ou stdinpar un StringIOsans avoir à apporter d'autres modifications à votre programme. Comme ils se StringIOconforment au même protocole IO, 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’étendent Objectà un moment donné).

Jörg W Mittag
la source
Il pourrait être utile que les langages autorisent les programmes à déclarer simultanément un type de classe et une interface pour laquelle cette classe est une implémentation, et permettent également aux implémentations de spécifier des "constructeurs" (qui enchaîneraient des constructeurs des classes spécifiées par l'interface). Pour les types d'objets pour lesquels les références seraient partagées publiquement, le modèle préféré serait que le type de classe ne soit utilisé que lors de la création de classes dérivées; la plupart des références doivent être du type interface. Pouvoir spécifier des constructeurs d'interface serait utile dans les cas où ...
supercat
Par exemple, le code a besoin d'une collection qui permette de lire un certain ensemble de valeurs par index, mais ne se soucie pas vraiment de son type. S'il existe de bonnes raisons de reconnaître les classes et les interfaces en tant que types distincts, il existe de nombreuses situations dans lesquelles ils devraient pouvoir travailler plus étroitement ensemble que ne le permettent actuellement les langues.
Supercat
Avez-vous une référence ou des mots-clés que je pourrais rechercher pour en savoir plus sur le dialecte expérimental LISP que vous avez mentionné, qui différencie formellement les mixins, les structures, les interfaces et les classes?
tel
@tel: Non, désolé. C'était probablement il y a 15 ou 20 ans, et à cette époque, mes intérêts étaient omniprésents. Je ne pourrais pas commencer à vous dire ce que je cherchais quand je suis tombé sur ça.
Jörg W Mittag
Awww. C'était le détail le plus intéressant de toutes ces réponses. Le fait qu’une séparation formelle de ces concepts soit réellement possible dans l’implémentation d’un langage a réellement contribué à cristalliser la distinction classe / type pour moi. Je suppose que je vais chercher ce LISP moi-même, en tout cas. Vous souvenez-vous si vous avez lu à ce sujet dans un article / livre de journal ou si vous en avez entendu parler dans une conversation?
tel
2

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 Stringparce 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 Bpartout, 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 ExtendedStringClasswith Stringcomme nous l'avons fait StringClasscar, après tout, c'est une toute nouvelle classe, nous n'avons pas eu à écrire autant. Cela nous permettrait de donner ExtendedStringClassun type incompatible avec Stringle point de vue du vérificateur de type.

De même, nous aurions pu décider de faire une toute nouvelle classe NewClasset faire

associate NewClass with String

Désormais, chaque instance de StringClasspeut être remplacée par celle NewClassdu 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 concatenateméthode du tout!

Bon, stipulons que chaque classe génère automatiquement un nouveau type du même nom que cette classe et associates instances de ce type. Cela nous permet de nous débarrasser des associatenoms différents entre StringClasset 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 ofle 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.

badcook
la source