Je sais que les structures dans .NET ne prennent pas en charge l'héritage, mais ce n'est pas exactement pourquoi elles sont limitées de cette manière.
Quelle raison technique empêche les structures d'hériter d'autres structures?
.net
inheritance
struct
Juliette
la source
la source
Réponses:
La raison pour laquelle les types de valeur ne peuvent pas prendre en charge l'héritage est à cause des tableaux.
Le problème est que, pour des raisons de performances et de GC, les tableaux de types valeur sont stockés «en ligne». Par exemple, étant donné que
new FooType[10] {...}
siFooType
est un type de référence, 11 objets seront créés sur le tas géré (un pour le tableau et 10 pour chaque instance de type). SiFooType
est plutôt un type valeur, une seule instance sera créée sur le tas géré - pour le tableau lui-même (car chaque valeur du tableau sera stockée "en ligne" avec le tableau).Maintenant, supposons que nous ayons un héritage avec des types valeur. Lorsqu'elles sont combinées avec le comportement de «stockage en ligne» ci-dessus des tableaux, de mauvaises choses se produisent, comme on peut le voir en C ++ .
Considérez ce code pseudo-C #:
Par les règles de conversion normales, a
Derived[]
est convertible en aBase[]
(pour le meilleur ou pour le pire), donc si vous s / struct / class / g pour l'exemple ci-dessus, il se compilera et fonctionnera comme prévu, sans problème. Mais siBase
etDerived
sont des types valeur, et que les tableaux stockent les valeurs en ligne, alors nous avons un problème.Nous avons un problème car
Square()
ne sait rienDerived
, il utilisera uniquement l'arithmétique du pointeur pour accéder à chaque élément du tableau, en incrémentant d'une quantité constante (sizeof(A)
). L'assemblage serait vaguement comme:(Oui, c'est un assemblage abominable, mais le fait est que nous allons incrémenter dans le tableau à des constantes de compilation connues, sans aucune connaissance qu'un type dérivé est utilisé.)
Donc, si cela se produisait réellement, nous aurions des problèmes de corruption de la mémoire. Plus précisément, à l'intérieur
Square()
,values[1].A*=2
serait en fait modifiervalues[0].B
!Essayez de déboguer CELA !
la source
Imaginez l'héritage supporté par les structures. Puis déclarant:
signifierait que les variables struct n'ont pas de taille fixe, et c'est pourquoi nous avons des types de référence.
Mieux encore, considérez ceci:
la source
Foo
HériterBar
ne devrait pas permettreFoo
d'être affecté à unBar
, mais déclarant une struct cette façon pourrait permettre à un couple d'effets utiles: (1) Créer un membre spécialement nommé de typeBar
comme le premier élémentFoo
, et ontFoo
notamment noms de membres qui correspondent à ces membres dansBar
, permettant au code qui avaitBar
été adapté d'utiliser à laFoo
place, sans avoir à remplacer toutes les références àthing.BarMember
parthing.theBar.BarMember
, et en conservant la capacité de lire et d'écrire tousBar
les champs de en tant que groupe; ...Les structures n'utilisent pas de références (à moins qu'elles ne soient encadrées, mais vous devriez essayer d'éviter cela) ainsi le polymorphisme n'a pas de sens puisqu'il n'y a pas d'indirection via un pointeur de référence. Les objets vivent normalement sur le tas et sont référencés via des pointeurs de référence, mais les structs sont alloués sur la pile (sauf s'ils sont encadrés) ou sont alloués "à l'intérieur" de la mémoire occupée par un type de référence sur le tas.
la source
Foo
qui a un champ de type structureBar
soit capable de considérerBar
les membres de s comme les siens, de sorte que unePoint3d
classe pourrait par exemple encapsuler unPoint2d xy
mais se référer auX
champ de ce champ comme étant soitxy.X
ouX
.Voici ce que disent les documents :
Fondamentalement, ils sont censés contenir des données simples et n'ont donc pas de "fonctionnalités supplémentaires" telles que l'héritage. Il serait probablement techniquement possible pour eux de prendre en charge un type d'héritage limité (pas de polymorphisme, car ils sont sur la pile), mais je pense que c'est aussi un choix de conception de ne pas prendre en charge l'héritage (comme beaucoup d'autres choses dans le .NET les langues sont.)
D'un autre côté, je suis d'accord avec les avantages de l'héritage, et je pense que nous avons tous atteint le point où nous voulons
struct
hériter d'un autre, et nous nous rendons compte que ce n'est pas possible. Mais à ce stade, la structure des données est probablement si avancée qu'elle devrait de toute façon être une classe.la source
Point3D
partir de aPoint2D
; vous ne seriez pas capable d'utiliser aPoint3D
au lieu de aPoint2D
, mais vous n'auriez pas à réimplémenterPoint3D
entièrement le tout à partir de zéro.) C'est ainsi que je l'ai interprété de toute façon ...class
lestruct
cas échéant.L'héritage de classe comme n'est pas possible, car une structure est posée directement sur la pile. Un struct héritant serait plus gros que son parent, mais le JIT ne le sait pas et essaie d'en mettre trop sur trop moins d'espace. Cela semble un peu flou, écrivons un exemple:
Si cela était possible, il planterait sur l'extrait suivant:
L'espace est alloué pour la taille de A, pas pour la taille de B.
la source
Il y a un point que je voudrais corriger. Même si la raison pour laquelle les structures ne peuvent pas être héritées est parce qu'elles vivent sur la pile est la bonne, c'est en même temps une explication à moitié correcte. Les structures, comme tout autre type de valeur, peuvent vivre dans la pile. Parce que cela dépendra de l'endroit où la variable est déclarée, elles vivront soit dans la pile, soit dans le tas . Ce sera respectivement lorsqu'il s'agit de variables locales ou de champs d'instance.
En disant cela, Cecil a un nom l'a bien cloué.
Je voudrais souligner ceci, les types de valeur peuvent vivre sur la pile. Cela ne veut pas dire qu'ils le font toujours. Les variables locales, y compris les paramètres de méthode, le seront. Tous les autres ne le feront pas. Néanmoins, cela reste la raison pour laquelle ils ne peuvent pas être hérités. :-)
la source
Les structures sont allouées sur la pile. Cela signifie que la sémantique des valeurs est pratiquement gratuite et que l'accès aux membres de structure est très bon marché. Cela n'empêche pas le polymorphisme.
Vous pouvez faire démarrer chaque structure par un pointeur vers sa table de fonctions virtuelles. Ce serait un problème de performances (chaque structure aurait au moins la taille d'un pointeur), mais c'est faisable. Cela permettrait des fonctions virtuelles.
Qu'en est-il d'ajouter des champs?
Eh bien, lorsque vous allouez une structure sur la pile, vous allouez une certaine quantité d'espace. L'espace requis est déterminé au moment de la compilation (que ce soit à l'avance ou lors du JITting). Si vous ajoutez des champs, puis attribuez-les à un type de base:
Cela écrasera une partie inconnue de la pile.
L'alternative est que le runtime empêche cela en n'écrivant que sizeof (A) octets dans n'importe quelle variable A.
Que se passe-t-il si B remplace une méthode dans A et référence son champ Integer2? Soit le runtime lève une exception MemberAccessException, soit la méthode accède à la place à des données aléatoires sur la pile. Aucun de ces éléments n'est autorisé.
Il est parfaitement sûr d'avoir l'héritage de struct, tant que vous n'utilisez pas de structure polymorphique, ou tant que vous n'ajoutez pas de champs lors de l'héritage. Mais ce ne sont pas très utiles.
la source
Cela semble être une question très fréquente. J'ai envie d'ajouter que les types de valeur sont stockés «à la place» où vous déclarez la variable; à part les détails d'implémentation, cela signifie qu'il n'y a pas d'en- tête d'objet qui dit quelque chose sur l'objet, seule la variable sait quel type de données y réside.
la source
Les structures prennent en charge les interfaces, vous pouvez donc faire certaines choses polymorphes de cette façon.
la source
IL est un langage basé sur la pile, donc appeler une méthode avec un argument ressemble à ceci:
Lorsque la méthode s'exécute, elle supprime certains octets de la pile pour obtenir son argument. Il sait exactement combien d'octets sortir car l'argument est soit un pointeur de type référence (toujours 4 octets sur 32 bits), soit un type valeur dont la taille est toujours connue avec précision.
S'il s'agit d'un pointeur de type référence, la méthode recherche l'objet dans le tas et obtient son descripteur de type, qui pointe vers une table de méthodes qui gère cette méthode particulière pour ce type exact. S'il s'agit d'un type valeur, aucune recherche dans une table de méthodes n'est nécessaire car les types valeur ne prennent pas en charge l'héritage, il n'y a donc qu'une seule combinaison méthode / type possible.
Si les types valeur supportaient l'héritage, il y aurait une surcharge supplémentaire en ce que le type particulier de la structure devrait être placé sur la pile ainsi que sa valeur, ce qui signifierait une sorte de recherche de table de méthodes pour l'instance concrète particulière du type. Cela éliminerait les avantages de vitesse et d'efficacité des types de valeur.
la source