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?
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?
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] {...}
si FooType
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). Si FooType
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 #:
struct Base
{
public int A;
}
struct Derived : Base
{
public int B;
}
void Square(Base[] values)
{
for (int i = 0; i < values.Length; ++i)
values [i].A *= 2;
}
Derived[] v = new Derived[2];
Square (v);
Par les règles de conversion normales, a Derived[]
est convertible en a Base[]
(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 si Base
et Derived
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 rien Derived
, 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:
for (int i = 0; i < values.Length; ++i)
{
A* value = (A*) (((char*) values) + i * sizeof(A));
value->A *= 2;
}
(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 modifier values[0].B
!
Essayez de déboguer CELA !
Imaginez l'héritage supporté par les structures. Puis déclarant:
BaseStruct a;
InheritedStruct b; //inherits from BaseStruct, added fields, etc.
a = b; //?? expand size during assignment?
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:
BaseStruct[] baseArray = new BaseStruct[1000];
baseArray[500] = new InheritedStruct(); //?? morph/resize the array?
Foo
Hériter Bar
ne devrait pas permettre Foo
d'être affecté à un Bar
, mais déclarant une struct cette façon pourrait permettre à un couple d'effets utiles: (1) Créer un membre spécialement nommé de type Bar
comme le premier élément Foo
, et ont Foo
notamment noms de membres qui correspondent à ces membres dans Bar
, permettant au code qui avait Bar
été adapté d'utiliser à la Foo
place, sans avoir à remplacer toutes les références à thing.BarMember
par thing.theBar.BarMember
, et en conservant la capacité de lire et d'écrire tous Bar
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.
Foo
qui a un champ de type structure Bar
soit capable de considérer Bar
les membres de s comme les siens, de sorte que une Point3d
classe pourrait par exemple encapsuler un Point2d xy
mais se référer au X
champ de ce champ comme étant soit xy.X
ou X
.
Voici ce que disent les documents :
Les structures sont particulièrement utiles pour les petites structures de données qui ont une sémantique de valeur. Les nombres complexes, les points dans un système de coordonnées ou les paires clé-valeur dans un dictionnaire sont tous de bons exemples de structs. La clé de ces structures de données est qu'elles ont peu de membres de données, qu'elles ne nécessitent pas l'utilisation d'héritage ou d'identité référentielle, et qu'elles peuvent être facilement implémentées à l'aide de la sémantique de valeur où l'affectation copie la valeur au lieu de la référence.
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.
Point3D
partir de a Point2D
; vous ne seriez pas capable d'utiliser a Point3D
au lieu de a Point2D
, mais vous n'auriez pas à réimplémenter Point3D
entièrement le tout à partir de zéro.) C'est ainsi que je l'ai interprété de toute façon ...
class
le struct
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:
struct A {
int property;
} // sizeof A == sizeof int
struct B : A {
int childproperty;
} // sizeof B == sizeof int * 2
Si cela était possible, il planterait sur l'extrait suivant:
void DoSomething(A arg){};
...
B b;
DoSomething(b);
L'espace est alloué pour la taille de A, pas pour la taille de B.
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. :-)
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:
struct A
{
public int Integer1;
}
struct B : A
{
public int Integer2;
}
A a = new B();
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.
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.
Les structures prennent en charge les interfaces, vous pouvez donc faire certaines choses polymorphes de cette façon.
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.