web-dev-qa-db-fra.com

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

La réponse la mieux notée à cette question sur le principe de substitution de Liskov prend soin de distinguer les termes sous-type et sous-classe . Il souligne également que certaines langues confondent les deux, tandis 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++, que signifierait une distinction entre sous-type et sous-classe? Supposons, par exemple, que Foo est une sous-classe, mais pas un sous-type, de FooBase. Si foo est une instance de Foo, cette ligne:

FooBase* fbPoint = &foo;

ne plus être valide?

45
tel

Le sous-typage est une forme de polymorphisme de type dans lequel un sous-type est un type de données qui est 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 fonctionner sur des éléments du supertype peuvent également fonctionner sur des éléments du sous-type.

Si S est un sous-type de T, la relation de sous-typage est souvent écrite S <: T, pour signifier que tout terme de type S peut être utilisé en toute sécurité dans un contexte où un terme de type T est attendu. La sémantique précise du sous-typage dépend de manière cruciale des détails de ce que "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 est-une, tandis que le sous-classement ne fait que réutiliser l'implémentation et établit une relation syntaxique, pas nécessairement une relation sémantique (l'héritage ne garantit pas le sous-typage comportemental).

Pour distinguer ces concepts, le sous-typage est également connu sous le nom héritage d'interface, tandis que le sous-classement est connu sous le nom héritage d'implémentation ou héritage de code. =

Références
Sous-typage
Héritage

53
Robert Harvey

Un type, dans le contexte dont nous parlons ici, est essentiellement un ensemble de garanties comportementales. A contrat, si vous voulez. Ou, empruntant la terminologie à Smalltalk, un protocole.

Un classe est un ensemble de méthodes. Il s'agit d'un ensemble de implémentations de comportement.

Sous-typage est un moyen d'affiner le protocole. Sous-classement est un moyen de réutilisation de code différentiel, c'est-à-dire de réutilisation de code en décrivant uniquement la différence de comportement.

Si vous avez utilisé Java ou C♯, alors vous avez peut-être rencontré le conseil que tous les types devraient être des types interface. En fait, si vous lisez William Cook On Understanding Data Abstraction, Revisited, alors vous savez peut-être que pour faire OO dans ces langues, vous devez utilisez uniquement interface s en tant que types. (En outre, fait amusant: Java cribbed interface s directement à partir des protocoles d'Objective-C, qui à leur tour sont tirés directement de Smalltalk.)

Maintenant, si nous suivons ce conseil de codage jusqu'à sa conclusion logique et imaginons une version de Java, où seulementinterface s sont des types, et les classes et les primitives ne le sont pas, alors un interface héritant d'un autre créera une relation de sous-typage, alors qu'un class héritant d'un autre sera simplement utilisé pour une réutilisation différentielle de code via super.

Pour autant que je sache, il n'y a pas de langages grand public statiquement typés qui distinguent strictement entre l'héritage code (héritage d'implémentation/sous-classement) et l'héritage contrats (sous-typage). Dans Java et C♯, l'héritage d'interface est un sous-typage pur (ou du moins l'était, jusqu'à l'introduction de méthodes par défaut dans Java 8 et probablement C♯ 8 comme bien), mais l'héritage de classe est aussi le sous-typage ainsi que l'héritage de l'implémentation. Je me souviens avoir lu à propos d'un dialecte LISP orienté objet expérimentalement typé statiquement, qui distinguait strictement mixins (qui contient comportement), structs (qui contiennent state), interfaces (qui décrivent comportement), et 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 comme types.

Dans un langage typé dynamiquement OO tel que Python, Ruby, ECMAScript ou Smalltalk, nous considérons généralement le (s) type (s) d'un objet comme l'ensemble des protocoles auxquels il se conforme. 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 type Object. (BTW: notez comment j'ai utilisé les noms de classe pour parler des types? C'est stupide de ma part!) Un objet peut implémenter plusieurs protocoles. Par exemple, dans Ruby, Arrays peut être ajouté à, ils peuvent être indexés, ils peuvent être itérés et peut être comparé. C'est quatre protocoles différents qu'ils implémentent!

Maintenant, Ruby n'a pas de types. Mais le Ruby communauté 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 produisant 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 type (qui n'existe que dans la tête du programmeur), alors il est autorisé pour mélanger (hériter de) le mixin Enumerable, et obtenir ainsi toutes sortes de méthodes intéressantes gratuitement, comme map, reduce, filter et bientôt.

De même, si un objet répond à <=>, alors il est envisagé d'implémenter le protocole comparable, et il peut se mélanger dans le mixin Comparable et obtenir des trucs comme <, <=, >, <=, ==, between? et clamp gratuitement. Cependant, il peut également implémenter toutes ces méthodes lui-même, et ne pas hériter du tout de Comparable, et il serait toujours considéré comparable.

Un bon exemple est la bibliothèque StringIO, qui essentiellement fakes flux d'E/S avec des chaînes. Il implémente toutes les mêmes méthodes que la classe IO, mais il n'y a pas de relation d'héritage entre les deux. Néanmoins, un StringIO peut être utilisé partout où un IO peut être utilisé. Ceci est très utile dans les tests unitaires, où vous pouvez remplacer un fichier ou stdin par un StringIO sans avoir à apporter d'autres modifications à votre programme. Puisque StringIO est conforme au même protocole que IO, ils sont tous les deux du même type, même s'ils sont de classes différentes, et ne partagent aucune relation (autre que la trivialité qu'ils étendent tous les deux Object à un moment donné).

28
Jörg W Mittag

Il est peut-être d'abord utile 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 aider à illustrer la différence entre un type et une classe parce que la plupart des langues les confondent au moins en partie (pour une 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 sa cohérence (pour un certain type de définition spécifique au système de cohérence) avec toutes les autres valeurs d'é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éritent leur propre nom.

Dans notre langage des jouets, nous pourrions permettre la création d'étiquettes de ce genre.

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 type peut désormais 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 cela est maladroit et comment vous allez avoir des problèmes pour attribuer un nombre infini de types d'expressions. On pourra y revenir plus tard.

Une classe, d'autre part, est une collection de méthodes et de champs qui sont regroupés (potentiellement avec des modificateurs d'accès tels que privés ou publics).

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, chaque classe n'ait pas de 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 du système de types si certaines expressions avaient des types et d'autres non.

De plus, dans notre langage du jouet, ces associations n'ont pas à être uniques. Nous pourrions associer deux classes avec le même type.

associate MyCustomStringClass with String

Maintenant, gardez à l'esprit que notre vérificateur de typographie n'est pas obligé de suivre la valeur d'une expression (et dans la plupart des cas, ce ne sera pas ou est 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 n'a pu que rejeter l'instruction 0 is of type String en raison de notre règle de type créée artificiellement selon laquelle les expressions doivent avoir des types uniques et nous avions déjà étiqueté l'expression 0 autre chose. Il n'avait aucune connaissance particulière de la valeur de 0.

Et le sous-typage? Eh bien, le sous-typage est le nom d'une règle courante dans le contrôle de frappe qui détend les autres règles que vous pourriez avoir. À savoir si A is subtype of B puis partout où votre typechecker exige une étiquette de B, il acceptera également un A.

Par exemple, nous pourrions faire ce qui suit pour nos chiffres 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 à String comme nous l'avons fait avec StringClass car, après tout, c'est une toute nouvelle classe, nous n'avons tout simplement pas eu à écrire autant. Cela nous permettrait de donner à ExtendedStringClass un type incompatible avec String du point de vue du vérificateur.

De même, nous aurions pu décider de créer une toute nouvelle classe NewClass et faire

associate NewClass with String

Désormais, chaque instance de StringClass peut être remplacée par NewClass du point de vue du vérificateur.

Donc, en théorie, le sous-typage et le sous-classement sont des choses complètement différentes. Mais aucun langage que je connais qui a des types et des classes ne fait les choses de cette façon. Commençons par réduire notre langage et expliquons la justification de certaines de nos décisions.

Tout d'abord, même si en théorie des classes complètement différentes peuvent recevoir le même type ou qu'une classe peut recevoir le même type que des valeurs qui ne sont des instances d'aucune classe, cela entrave sérieusement l'utilité du vérificateur de typage. Le vérificateur de frappe 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 aimeriez si vous avez la difficulté de jouer avec un typechecker. Après tout, qui sait quelle est réellement la valeur sous cette étiquette String; ce pourrait être quelque chose qui n'a pas, par exemple, une méthode concatenate du tout!

Bon, supposons donc que chaque classe génère automatiquement un nouveau type du même nom que cette classe et les instances de associates avec ce type. Cela nous permet de nous débarrasser de associate ainsi que des différents noms entre StringClass et String.

Pour la même raison, nous voulons probablement établir automatiquement une relation de sous-type entre les types de deux classes où l'une est une sous-classe d'une autre. Après tout, la sous-classe est garantie d'avoir toutes les méthodes et tous les champs de la classe parent, mais l'inverse n'est pas vrai. Par conséquent, bien que la sous-classe puisse passer à tout moment lorsque vous avez besoin d'un type de 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 avoir is subclass of tirer le double devoir et se débarrasser de is subtype of.

Et cela nous amène aux caractéristiques que la plupart des langues populaires typées statiquement OO partagent. Il existe un ensemble de types "primitifs" (par exemple int, float, etc.) qui ne sont associés à aucune classe et qui 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 le sous-classement avec le sous-typage.

La dernière note que je ferai concerne la maladresse de 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 pour générer des valeurs entièrement nouvelles qui sont automatiquement étiquetées avec ce type. Par exemple, une déclaration de classe crée généralement le type ainsi qu'un moyen d'instancier des valeurs de ce type. Cela supprime une partie de la lourdeur et, en présence de constructeurs, vous permet également de créer une étiquette infiniment de valeurs avec un type en un seul coup.

2
badcook