Je sais que les structures dans .NET ne supportent pas l'héritage, mais ce n'est pas très clair. pourquoi ils sont limités de cette manière.
Quelle raison technique empêche les structs d'hériter d'autres structs ?
Je sais que les structures dans .NET ne supportent pas l'héritage, mais ce n'est pas très clair. pourquoi ils sont limités de cette manière.
Quelle raison technique empêche les structs d'hériter d'autres structs ?
La raison pour laquelle les types de valeurs ne peuvent pas supporter l'héritage est due aux tableaux.
Le problème est que, pour des raisons de performances et de GC, les tableaux de types de valeurs sont stockés "en ligne". Par exemple, étant donné 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 de valeur, une seule instance sera créée sur le tas géré -- pour le tableau lui-même (puisque chaque valeur du tableau sera stockée "en ligne" avec le tableau).
Maintenant, supposons que nous ayons un héritage avec des types de valeurs. Lorsqu'il est combiné avec le comportement de "stockage en ligne" des tableaux, les 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);
Selon les règles normales de conversion, un Derived[]
est convertible en un Base[]
(pour le meilleur et pour le pire), donc si vous utilisez s/struct/class/g pour l'exemple ci-dessus, il se compilera et s'exécutera comme prévu, sans problème. Mais si vous Base
y Derived
sont des types de valeurs, et que les tableaux stockent des valeurs en ligne, alors nous avons un problème.
Nous avons un problème parce que Square()
ne sait rien sur Derived
il utilisera uniquement l'arithmétique des pointeurs pour accéder à chaque élément du tableau, en l'incrémentant d'une quantité constante ( sizeof(A)
). L'assemblage ressemblerait vaguement à :
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 point est que nous allons incrémenter à travers 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 mémoire. Plus précisément, dans Square()
, values[1].A*=2
serait en fait modifier values[0].B
¡!
Essayez de déboguer QUE ¡!
Imaginez que les structs supportent l'héritage. Puis déclarer :
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.
Encore mieux, considérez ceci :
BaseStruct[] baseArray = new BaseStruct[1000];
baseArray[500] = new InheritedStruct(); //?? morph/resize the array?
Les structures n'utilisent pas de références (à moins qu'elles ne soient encadrées, mais vous devriez essayer d'éviter cela), donc 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 structures sont allouées sur la pile (à moins qu'elles ne soient encadrées) ou sont allouées "à l'intérieur" de la mémoire occupée par un type de référence sur le tas.
Voici ce que les docs disent:
Les structures sont particulièrement utiles pour les petites structures de données qui ont une valeur sémantique. Les nombres complexes, les points dans un système de coordonnées, ou de paires clé-valeur dans un dictionnaire sont tous de bons exemples de structures. La clé de ces structures de données est qu'elles ont peu de membres de données, qu'ils ne nécessitent pas l'utilisation de l'héritage ou référentiel d'identité, et qu'ils peuvent être facilement mis en œuvre à l'aide de la valeur sémantique où l'affectation des copies de la valeur à la place de la référence.
Fondamentalement, ils sont censés détenir des données simple et, par conséquent, n'ont pas de "fonctions supplémentaires" comme l'héritage. Il serait sans doute techniquement possible pour eux de soutenir certains limité de l'héritage (pas de polymorphisme, car elles sont sur la pile), mais je crois que c'est aussi un choix de conception pour ne pas supporter l'héritage (comme beaucoup d'autres choses dans le .NET sont les langues.)
En revanche, je suis d'accord avec les avantages de l'héritage, et je pense que nous avons tous atteint le point où nous voulons que notre struct
d'hériter d'une autre, et de réaliser qu'il n'est pas possible. Mais à ce moment, la structure de données est probablement tellement avancé qu'il devrait y avoir une classe de toute façon.
Les structures sont allouées sur la pile. Cela signifie que la sémantique des valeurs est pratiquement libre, et que l'accès aux membres de la structure est très bon marché. Cela n'empêche pas le polymorphisme.
On pourrait faire en sorte que chaque structure commence par un pointeur vers sa table de fonctions virtuelles. Cela poserait un problème de performances (chaque structure aurait au moins la taille d'un pointeur), mais c'est faisable. Cela permettrait d'avoir des fonctions virtuelles.
Qu'en est-il de l'ajout de champs ?
Eh bien, lorsque vous allouez une structure sur la pile, vous allouez une certaine quantité d'espace. L'espace nécessaire est déterminé au moment de la compilation (que ce soit à l'avance ou lors du JITting). Si vous ajoutez des champs et les affectez ensuite à un type de base :
struct A
{
public int Integer1;
}
struct B : A
{
public int Integer2;
}
A a = new B();
Cela va écraser une partie inconnue de la pile.
L'alternative est que le runtime empêche cela en écrivant seulement sizeof(A) bytes à toute variable A.
Que se passe-t-il si B surcharge une méthode de A et fait référence à son champ Integer2 ? Soit le runtime lève une MemberAccessException, soit la méthode accède à des données aléatoires sur la pile. Aucun de ces cas n'est autorisé.
Il est parfaitement sûr d'avoir un héritage de structures, tant que vous n'utilisez pas les structures de manière polymorphe, ou tant que vous n'ajoutez pas de champs lors de l'héritage. Mais cela n'est pas terriblement utile.
Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.