Pourquoi est-ce correct et surtout attendu:
abstract type Shape
{
abstract number Area();
}
concrete type Triangle : Shape
{
concrete number Area()
{
//...
}
}
... alors que ce n'est pas correct et que personne ne se plaint:
concrete type Name : string
{
}
concrete type Index : int
{
}
concrete type Quantity : int
{
}
Ma motivation est de maximiser l'utilisation du système de types pour la vérification de l'exactitude à la compilation.
PS: oui, j'ai lu ceci et l'emballage est une solution de rechange .
Réponses:
Je suppose que vous pensez à des langages comme Java et C #?
Dans ces langues, les primitives (comme
int
) sont fondamentalement un compromis pour la performance. Ils ne prennent pas en charge toutes les fonctionnalités des objets, mais ils sont plus rapides et nécessitent moins de temps système.Pour que les objets prennent en charge l'héritage, chaque instance doit "savoir" au moment de l'exécution quelle classe elle est une instance. Sinon, les méthodes remplacées ne peuvent pas être résolues au moment de l'exécution. Pour les objets, cela signifie que les données d'instance sont stockées en mémoire avec un pointeur sur l'objet de classe. Si de telles informations devaient également être stockées avec des valeurs primitives, les besoins en mémoire augmenteraient. Une valeur entière de 16 bits nécessiterait ses 16 bits pour la valeur et en plus une mémoire de 32 ou 64 bits pour un pointeur sur sa classe.
Outre la surcharge de mémoire, vous devriez également être en mesure de remplacer les opérations courantes sur les primitives telles que les opérateurs arithmétiques. Sans sous-typage, les opérateurs similaires
+
peuvent être compilés en une simple instruction de code machine. Si cela pouvait être remplacé, vous auriez besoin de résoudre les méthodes au moment de l'exécution, opération beaucoup plus coûteuse. (Vous savez peut-être que C # prend en charge la surcharge des opérateurs, mais ce n'est pas la même chose. La surcharge des opérateurs est résolue au moment de la compilation. Il n'y a donc pas de pénalité d'exécution par défaut.)Les chaînes ne sont pas des primitives, mais elles restent "spéciales" dans la façon dont elles sont représentées en mémoire. Par exemple, ils sont "internés", ce qui signifie que deux chaînes de caractères identiques peuvent être optimisées pour la même référence. Cela ne serait pas possible (ou du moins beaucoup moins efficace) si les instances de chaîne devaient également garder une trace de la classe.
Ce que vous décrivez serait certes utile, mais sa prise en charge nécessiterait une surcharge de performances pour chaque utilisation de primitives et de chaînes, même lorsqu'elles ne tirent pas parti de l'héritage.
Le langage Smalltalk fait (je crois) d'entiers permet le sous. Mais lors de la conception de Java, Smalltalk était considéré comme trop lent et le fait de tout avoir comme objet était considéré comme l'une des principales raisons. Java a sacrifié de l'élégance et de la pureté conceptuelle pour obtenir de meilleures performances.
la source
string
est scellé car il est conçu pour se comporter de manière immuable. Si l'on pouvait hériter d'une chaîne, il serait possible de créer des chaînes mutables, ce qui la rendrait vraiment sujette aux erreurs. Des tonnes de code, y compris le framework .NET lui-même, reposent sur des chaînes sans effets secondaires. Voir aussi ici, vous dit la même chose: quora.com/Why-String-class-in-C-is-a-sealed-classString
est marquéefinal
en Java aussi.string
C♯ n'est que du sable? Non, bien sûr que non, unstring
en C♯ est ce que dit la spécification C says. La façon dont il est mis en œuvre est sans importance. Une implémentation native de C♯ implémenterait des chaînes sous forme de tableaux d'octets, une implémentation ECMAScript les mapperait versString
s ECMAScript , etc.Ce que propose un langage n’est pas un sous-classement, mais un sous-typage . Par exemple, Ada vous permet de créer des types dérivés ou des sous - types . La section Ada Programming / Type System vaut la peine d’être lue pour comprendre tous les détails. Vous pouvez restreindre la plage de valeurs, ce que vous souhaitez la plupart du temps:
Vous pouvez utiliser les deux types sous forme d'entiers si vous les convertissez explicitement. Notez également que vous ne pouvez pas utiliser l'un à la place de l'autre, même lorsque les plages sont structurellement équivalentes (les types sont vérifiés par leur nom).
Les types ci-dessus sont incompatibles, même s'ils représentent la même plage de valeurs.
(Mais vous pouvez utiliser Unchecked_Conversion; ne dites pas aux gens que je vous ai dit ça)
la source
type Index is -MAXINT..MAXINT;
ce qui en quelque sorte ne fait rien pour moi comme tous les entiers seraient valables? Alors, quel genre d'erreur pourrais-je obtenir en passant un Angle à un Index si tout ce qui est vérifié est la plage?Je pense que cela pourrait très bien être une question X / Y. Points saillants, de la question ...
... et de votre commentaire en précisant:
Excusez-moi si quelque chose me manque, mais ... Si ce sont vos objectifs, alors pourquoi parlez-vous d'héritage? La substituabilité implicite est ... comme ... tout son contenu. Vous savez, le principe de substitution de Liskov?
Ce que vous semblez vouloir, en réalité, est le concept de "typedef fort" - par lequel quelque chose "est" par exemple un
int
en termes de gamme et de représentation mais ne peut pas être substitué dans des contextes qui attendent unint
et vice-versa. Je suggérerais de rechercher des informations sur ce terme et quelle que soit la langue choisie par votre langue. Encore une fois, c'est à peu près littéralement le contraire de l'héritage.Et pour ceux qui n'aimeraient peut-être pas une réponse X / Y, je pense que le titre pourrait encore être rendu compte du LSP. Les types primitifs sont primitifs parce qu'ils font quelque chose de très simple, et c'est tout ce qu'ils font . Leur permettre d'être hérités et ainsi rendre infinis leurs effets possibles conduirait au mieux à une grande surprise et au pire à une violation fatale. Si je peux présumer avec optimisme que Thales Pereira ne voudra pas que je cite ce commentaire phénoménal:
Si quelqu'un voit un type primitif, dans un langage sain d'esprit, il présume à juste titre qu'il fera toujours son petit geste, très bien, sans surprises. Les types primitifs n'ont pas de déclaration de classe disponible indiquant si ils peuvent ou non être hérités et dont les méthodes sont remplacées. S'ils l'étaient, ce serait vraiment très surprenant (et de briser totalement la compatibilité en amont, mais je suis conscient que c'est une réponse en arrière à «pourquoi X n'a-t-il pas été conçu avec Y»).
... bien que, comme Mooing Duck l’a souligné en réponse, les langages autorisant la surcharge des opérateurs permettent à l’utilisateur de se confondre dans une mesure similaire ou égale s’ils le souhaitent vraiment, il est donc douteux que ce dernier argument soit valable. Et je vais arrêter de résumer les commentaires des autres maintenant, heh.
la source
Afin de permettre l'héritage avec l'envoi virtuel 8 (ce qui est souvent considéré comme très souhaitable dans la conception d'applications), il est nécessaire de disposer d'informations de type à l'exécution. Pour chaque objet, certaines données concernant le type de l'objet doivent être stockées. Une primitive, par définition, manque de cette information.
Il existe deux langages OOP grand public (gérés, exécutés sur une machine virtuelle) qui comportent des primitives: C # et Java. Beaucoup d'autres langues n'ont pas de primitives en premier lieu, ou utilisent un raisonnement similaire pour les autoriser / les utiliser.
Les primitives sont un compromis pour la performance. Pour chaque objet, vous avez besoin d’espace pour son en-tête (En Java, généralement 2 * 8 octets sur les ordinateurs virtuels 64 bits), de ses champs et d’un remplissage éventuel (En Hotspot, chaque objet occupe un nombre d'octets multiple de 8). Ainsi, un
int
objet as aurait besoin d’au moins 24 octets de mémoire, au lieu de 4 octets (en Java).Ainsi, des types primitifs ont été ajoutés pour améliorer les performances. Ils facilitent beaucoup de choses. Que
a + b
signifie si les deux sont des sous-types deint
? Une sorte de dispathcing doit être ajouté pour choisir le bon ajout. Cela signifie une répartition virtuelle. Avoir la possibilité d'utiliser un opcode très simple pour l'ajout est beaucoup plus rapide et permet des optimisations au moment de la compilation.String
est un autre cas. En Java et en C #,String
c’est un objet. Mais en C #, il est scellé et en Java, il est définitif. Cela parce que les bibliothèques standard Java et C # exigent queString
s soit immuable, et les sous-classer briserait cette immuabilité.Dans le cas de Java, la VM peut (et fait) interner des chaînes et les "mettre en pool", ce qui permet de meilleures performances. Cela ne fonctionne que lorsque les chaînes sont vraiment immuables.
De plus, il est rarement nécessaire de sous-classer les types primitifs. Tant que les primitives ne peuvent pas être sous-classées, les maths nous en disent beaucoup. Par exemple, nous pouvons être sûrs que l'addition est commutative et associative. C’est quelque chose que la définition mathématique d’entiers nous dit. De plus, on peut facilement prédier les invariants sur des boucles par induction dans de nombreux cas. Si nous autorisons le sous-classement de
int
, nous perdons les outils fournis par les mathématiques, car nous ne pouvons plus garantir que certaines propriétés sont valables. Ainsi, je dirais que la possibilité de ne pas pouvoir sous-classer les types primitifs est en fait une bonne chose. Moins de choses peuvent fuir, plus un compilateur peut souvent prouver qu'il est autorisé à faire certaines optimisations.la source
to allow inheritance, one needs runtime type information.
Faux.For every object, some data regarding the type of the object has to be stored.
Faux.There are two mainstream OOP languages that feature primitives: C# and Java.
Quoi, le C ++ n'est-il pas courant maintenant? Je vais l'utiliser comme réfutation, car les informations de type à l'exécution sont un terme C ++. Ce n'est absolument pas nécessaire à moins d'utiliserdynamic_cast
outypeid
. Et même si les RTTI sont activés, l'héritage ne consomme de l'espace que si une classe a desvirtual
méthodes auxquelles une table de méthodes par classe doit être pointée par instancedynamic_cast
et l'typeinfo
exiger. La répartition virtuelle est pratiquement mise en œuvre à l'aide d'un pointeur sur la table vtable pour la classe concrète de l'objet, permettant ainsi d'appeler les fonctions appropriées, mais sans exiger les détails de type et de relation inhérents à RTTI. Tout ce que le compilateur a besoin de savoir, c'est si la classe d'un objet est polymorphe et, dans l'affirmative, quel est le vptr de l'instance. On peut trivialement compiler des classes virtuellement expédiées avec-fno-rtti
.dynamic_cast
les classes sans envoi virtuel. La raison de la mise en œuvre est que RTTI est généralement implémenté en tant que membre caché d'une table virtuelle.Dans les langages POO statiques forts classiques, le sous-typage est principalement considéré comme un moyen d'étendre un type et de remplacer les méthodes actuelles du type.
Pour ce faire, les «objets» contiennent un pointeur sur leur type. Il s'agit d'une surcharge: le code dans une méthode qui utilise une
Shape
instance doit d'abord accéder aux informations de type de cette instance avant de connaître laArea()
méthode correcte à appeler.Une primitive a tendance à n'autoriser que des opérations pouvant être traduites en instructions informatiques simples et ne comportant aucune information de type. Rendre un nombre entier plus lent afin que quelqu'un puisse sous-classer cette classe était assez peu attrayant pour empêcher toute langue qui le deviendrait de devenir la norme.
Donc, la réponse à:
Est:
Cependant, nous commençons à avoir des langages qui permettent une vérification statique basée sur des propriétés de variables autres que 'type', par exemple F # a "dimension" et "unité" de sorte que vous ne pouvez pas, par exemple, ajouter une longueur à une zone. .
Il existe également des langages qui autorisent les «types définis par l'utilisateur» qui ne changent pas (ni n'échangent) ce qu'un type fait, mais aident simplement à la vérification de type statique; voir la réponse de coredump.
la source
Je ne suis pas sûr d'oublier quelque chose, mais la réponse est assez simple:
Notez qu'il n'existe en réalité que deux langages POO statiques forts qui ont même des primitives, AFAIK: Java et C ++. (En fait, je ne suis même pas sûr de ce dernier, je ne connais pas grand chose au C ++, et ce que j'ai trouvé en cherchant était déroutant.)
En C ++, les primitives sont essentiellement un héritage hérité (jeu de mots) de C. Elles ne participent donc pas au système d'objets (et donc à l'héritage) car C n'a ni système d'objet ni héritage.
En Java, les primitives résultent d’une tentative erronée d’améliorer les performances. Les primitives sont également les seuls types de valeur du système. En fait, il est impossible d'écrire des types de valeur en Java et il est impossible que les objets soient des types de valeur. Donc, mis à part le fait que les primitives ne participent pas au système d'objet et que l'idée "d'héritage" n'a même pas de sens, même si vous pouviez en hériter, vous ne pourriez pas conserver le " valeur-ness ". Ceci est différent de par exemple C♯ qui n'ont les types de valeurs ( S), qui sont tout de même des objets.
struct
Une autre chose est que ne pas être en mesure d'hériter n'est en réalité pas unique aux primitives. Dans C♯,
struct
s hérite implicitement deSystem.Object
et peut implémenterinterface
s, mais ils ne peuvent ni hériter ni hérité declass
es ou destruct
s. En outre,sealed
class
es ne peut pas être hérité de. En Java,final
class
il ne peut pas en être hérité.tl; dr :
final
ousealed
en Java ou C♯,struct
s en C,case class
es en Scala)la source
virtual
, ce qui signifie qu'elles n'obéissent pas à LSP. Egstd::string
n'est pas un primitif, mais cela se comporte plutôt comme une valeur supplémentaire. Une telle sémantique de valeur est assez courante, l’ensemble de la partie STL de C ++ l’assume.int
utilisation. Chaque allocation prend environ 100 nbs plus les frais généraux du ramassage des ordures. Comparez cela avec le seul cycle de processeur consommé en ajoutant deux primitivesint
. Vos codes java ramperaient si les concepteurs de la langue en avaient décidé autrement.int
, de sorte qu’ils fonctionnent exactement de la même manière. (Scala-native les compile dans des registres machine primitifs, Scala.js les compile dans des ECMAScriptNumber
s primitifs .) Ruby n'a pas de primitives, mais YARV et Rubinius compilent des entiers en machines primitives, JRuby les compile enlong
s primitives JVM . Pratiquement toutes les implémentations Lisp, Smalltalk ou Ruby utilisent des primitives dans la VM . C'est là que l'optimisation des performances…Joshua Bloch dans «Effective Java» recommande de concevoir explicitement pour l'héritage ou l'interdire. Les classes primitives ne sont pas conçues pour l'héritage, car elles sont conçues pour être immuables et permettre un héritage pourrait changer cela dans les sous-classes, brisant ainsi le principe de Liskov et ce serait une source de nombreux bugs.
Quoi qu'il en soit, pourquoi est- ce une solution de contournement? Vous devriez vraiment préférer la composition à l'héritage. Si la raison en est la performance, vous avez un point et la réponse à votre question est qu'il n'est pas possible de mettre toutes les fonctionnalités en Java, car il faut du temps pour analyser tous les aspects de l'ajout d'une fonctionnalité. Par exemple, Java n'avait pas de génériques avant la 1.5.
Si vous avez beaucoup de patience, vous avez de la chance, car il est prévu d'ajouter des classes de valeur à Java, ce qui vous permettra de créer vos classes de valeur, ce qui vous aidera à augmenter les performances tout en vous offrant davantage de flexibilité.
la source
Au niveau abstrait, vous pouvez inclure tout ce que vous voulez dans la langue que vous concevez.
Au niveau de la mise en œuvre, il est inévitable que certaines de ces choses soient plus simples à mettre en œuvre, que certaines soient compliquées, que certaines soient rapides, que certaines soient forcément plus lentes, etc. Pour en tenir compte, les concepteurs doivent souvent prendre des décisions difficiles et faire des compromis.
Au niveau de la mise en œuvre, l’un des moyens les plus rapides pour accéder à une variable est de trouver son adresse et de charger le contenu de cette adresse. Il existe des instructions spécifiques dans la plupart des CPU pour le chargement des données à partir d’adresses et ces instructions ont généralement besoin de savoir combien d’octets elles doivent charger (un, deux, quatre, huit, etc.) et où placer les données qu’elles chargent (registre unique, registre). paire, registre étendu, autre mémoire, etc.). En connaissant la taille d'une variable, le compilateur peut savoir exactement quelle instruction émettre pour les utilisations de cette variable. En ignorant la taille d'une variable, le compilateur devrait recourir à quelque chose de plus compliqué et probablement plus lent.
Au niveau abstrait, le point de sous-typage est de pouvoir utiliser des instances d'un type où un type égal ou plus général est attendu. En d'autres termes, il est possible d'écrire du code qui attend un objet d'un type particulier ou quoi que ce soit de plus dérivé, sans savoir à l'avance ce que ce serait exactement. Et, de toute évidence, étant donné que plus de types dérivés peuvent ajouter plus de membres de données, un type dérivé n’a pas nécessairement les mêmes besoins en mémoire que ses types de base.
Au niveau de la mise en œuvre, il n’existe aucun moyen simple pour une variable d’une taille prédéterminée de contenir une instance de taille inconnue et d’y accéder de la manière que vous appelleriez normalement efficace. Mais il existe un moyen de déplacer un peu les choses et d'utiliser une variable non pour stocker l'objet, mais pour identifier l'objet et laisser cet objet être stocké ailleurs. De cette façon, il s’agit d’une référence (par exemple une adresse de mémoire) - un niveau supplémentaire d’indirection qui garantit qu’une variable doit seulement contenir une sorte d’information de taille fixe, tant que nous pouvons trouver l’objet à travers cette information. Pour ce faire, il suffit de charger l’adresse (taille fixe), puis de travailler comme d’habitude en utilisant les décalages de l’objet que nous savons valables, même si cet objet a plus de données en décalage que nous ne connaissons pas. Nous pouvons le faire parce que nous ne
Au niveau abstrait, cette méthode vous permet de stocker une (référence à a)
string
dans uneobject
variable sans perdre les informations qui la rendent astring
. Cela convient à tous les types de travailler comme ça et vous pourriez aussi dire que c'est élégant à bien des égards.Néanmoins, au niveau de la mise en œuvre, le niveau supplémentaire d'indirection implique plus d'instructions et rend la plupart des architectures plus lentes chaque accès à l'objet. Vous pouvez autoriser le compilateur à augmenter les performances d'un programme si vous incluez dans votre langage certains types couramment utilisés qui ne possèdent pas ce niveau supplémentaire d'indirection (la référence). Mais en supprimant ce niveau d'indirection, le compilateur ne peut plus vous permettre de sous-taper de manière sécurisée pour la mémoire. En effet, si vous ajoutez plus de membres de données à votre type et que vous affectez un type plus général, tous les membres de données supplémentaires qui ne rentrent pas dans l'espace alloué à la variable cible seront découpés en tranches.
la source
En général
Si une classe est abstraite (métaphore: une boîte avec un (des) trou (s)), il est OK (même nécessaire d'avoir quelque chose utilisable!) Pour "combler le (s) trou (s)", c'est pourquoi nous sous-classons les classes abstraites.
Si une classe est concrète (métaphore: une boîte pleine), il n'est pas correct de modifier l'existant, car si elle est complète, elle est saturée. Nous n'avons pas la possibilité d'ajouter quelque chose de plus dans la boîte, c'est pourquoi nous ne devrions pas sous-classer les classes concrètes.
Avec des primitives
Les primitives sont des classes concrètes par conception. Ils représentent quelque chose de connu, parfaitement défini (je n'ai jamais vu de type primitif avec quelque chose d'abstrait, sinon ce n'est plus un primitif) et largement utilisé dans le système. Permettre de sous-classer un type primitif et de fournir sa propre implémentation à d'autres qui s'appuient sur le comportement conçu par les primitives peut entraîner de nombreux effets secondaires et des dégâts énormes!
la source
Généralement, l'héritage n'est pas la sémantique que vous souhaitez, car vous ne pouvez pas remplacer votre type spécial là où une primitive est attendue. Pour emprunter à votre exemple, a
Quantity + Index
n'a pas de sens sémantiquement, donc une relation d'héritage est une mauvaise relation.Cependant, plusieurs langues ont le concept d'un type de valeur qui exprime le type de relation que vous décrivez. Scala est un exemple. Un type de valeur utilise une primitive comme représentation sous-jacente, mais a une identité de classe et des opérations différentes à l'extérieur. Cela a pour effet d'étendre un type primitif, mais il s'agit davantage d'une composition plutôt que d'une relation d'héritage.
la source