Pourquoi les structures ne prennent-elles pas en charge l'héritage?

128

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?

Juliette
la source
6
Je ne meurs pas d'envie de cette fonctionnalité, mais je peux penser à quelques cas où l'héritage de structure serait utile: vous pourriez vouloir étendre une structure Point2D à une structure Point3D avec héritage, vous pourriez vouloir hériter d'Int32 pour contraindre ses valeurs entre 1 et 100, vous voudrez peut-être créer un type-def qui est visible sur plusieurs fichiers (l'astuce Utilisation de typeA = typeB a une portée de fichier uniquement), etc.
Juliet
4
Vous voudrez peut-être lire stackoverflow.com/questions/1082311/… , qui explique un peu plus sur les structures et pourquoi elles devraient être limitées à une certaine taille. Si vous souhaitez utiliser l'héritage dans une structure, vous devriez probablement utiliser une classe.
Justin
1
Et vous voudrez peut-être lire stackoverflow.com/questions/1222935/ ... car il explique en détail pourquoi cela ne peut tout simplement pas être fait sur la plate-forme dotNet. Ils ont froidement fait le chemin C ++, avec les mêmes problèmes qui peuvent être désastreux pour une plateforme gérée.
Dykam
Les classes @Justin ont des coûts de performance que les structures peuvent éviter. Et dans le développement de jeux, c'est vraiment important. Donc, dans certains cas, vous ne devriez pas utiliser une classe si vous pouvez l'aider.
Gavin Williams
@Dykam Je pense que cela peut être fait en C #. Désastreux est une exagération. Je peux écrire du code désastreux aujourd'hui en C # quand je ne suis pas familier avec une technique. Ce n'est donc pas vraiment un problème. Si l'héritage struct peut résoudre certains problèmes et donner de meilleures performances dans certains scénarios, alors je suis tout à fait d'accord.
Gavin Williams

Réponses:

121

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 FooTypeest 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 FooTypeest 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 Baseet Derivedsont 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*=2serait en fait modifier values[0].B!

Essayez de déboguer CELA !

Jonp
la source
11
La solution sensée à ce problème serait d'interdire la fonte de la forme Base [] en Detived []. Tout comme le casting de short [] en int [] est interdit, bien que le casting de short en int soit possible.
Niki
3
+ réponse: le problème d'héritage n'a pas cliqué avec moi jusqu'à ce que vous le mettiez en termes de tableaux. Un autre utilisateur a déclaré que ce problème pourrait être atténué en "découpant" les structures à la taille appropriée, mais je vois le découpage comme étant la cause de plus de problèmes qu'il n'en résout.
Juliet
4
Oui, mais cela "a du sens" car les conversions de tableau sont destinées à des conversions implicites, pas à des conversions explicites. short en int est possible, mais nécessite une conversion, il est donc judicieux que short [] ne puisse pas être converti en int [] (court de code de conversion, comme 'a.Select (x => (int) x) .ToArray ( ) '). Si le runtime interdisait la conversion de base en dérivé, ce serait une "verrue", car cela est autorisé pour les types de référence. Nous avons donc deux "verrues" différentes possibles - interdire l'héritage de struct ou interdire les conversions de tableaux de dérivés en tableaux de base.
jonp
3
Au moins en empêchant l'héritage des structures, nous avons un mot-clé séparé et pouvons plus facilement dire "les structures sont spéciales", par opposition à une limitation "aléatoire" dans quelque chose qui fonctionne pour un ensemble de choses (classes) mais pas pour un autre (structures) . J'imagine que la limitation des structures est beaucoup plus facile à expliquer ("ils sont différents!").
jonp
2
besoin de changer le nom de la fonction de «carré» à «double»
Jean
69

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?
Kenan EK
la source
5
C ++ a répondu à cela en introduisant le concept de «slicing», donc c'est un problème résoluble. Alors, pourquoi l'héritage struct ne devrait-il pas être pris en charge?
jonp
1
Considérez des tableaux de structures héritables et rappelez-vous que C # est un langage géré (en mémoire). Le tranchage ou toute autre option similaire ferait des ravages sur les fondamentaux du CLR.
Kenan EK
4
@jonp: Solvable, oui. Souhaitable? Voici une expérience de réflexion: imaginez si vous avez une classe de base Vector2D (x, y) et une classe dérivée Vector3D (x, y, z). Les deux classes ont une propriété Magnitude qui calcule respectivement sqrt (x ^ 2 + y ^ 2) et sqrt (x ^ 2 + y ^ 2 + z ^ 2). Si vous écrivez 'Vector3D a = Vector3D (5, 10, 15); Vector2D b = a; ', que doit renvoyer' a.Magnitude == b.Magnitude '? Si nous écrivons alors 'a = (Vector3D) b', est-ce que a.Magnitude a la même valeur avant l'affectation qu'après? Les concepteurs de .NET se sont probablement dit: "non, nous n'aurons rien de tout cela".
Juliet
1
Ce n'est pas parce qu'un problème peut être résolu qu'il doit être résolu. Parfois, il est simplement préférable d'éviter les situations où le problème survient.
Dan Diplo
@ kek444: Avoir struct FooHériter Barne devrait pas permettre Food'ê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 Barcomme le premier élément Foo, et ont Foonotamment noms de membres qui correspondent à ces membres dans Bar, permettant au code qui avait Barété adapté d'utiliser à la Fooplace, sans avoir à remplacer toutes les références à thing.BarMemberpar thing.theBar.BarMember, et en conservant la capacité de lire et d'écrire tous Barles champs de en tant que groupe; ...
supercat
14

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.

Martin Liversage
la source
8
on n'a pas besoin d'utiliser le polymorphisme pour profiter de l'héritage
rmeador
Alors, vous auriez combien de types d'héritage différents dans .NET?
John Saunders
Le polymorphisme existe dans les structures, considérez simplement la différence entre l'appel de ToString () lorsque vous l'implémentez sur une structure personnalisée ou lorsqu'une implémentation personnalisée de ToString () n'existe pas.
Kenan EK
C'est parce qu'ils dérivent tous de System.Object. C'est plus le polymorphisme du type System.Object que celui des structures.
John Saunders
Le polymorphisme pourrait être significatif avec des structures utilisées comme paramètres de type générique. Le polymorphisme fonctionne avec des structures qui implémentent des interfaces; le plus gros problème avec les interfaces est qu'elles ne peuvent pas exposer les byrefs aux champs de structure. Sinon, la chose la plus importante que je pense serait utile dans la mesure où "hériter" des structures serait un moyen d'avoir un type (struct ou class) Fooqui a un champ de type structure Barsoit capable de considérer Barles membres de s comme les siens, de sorte que une Point3dclasse pourrait par exemple encapsuler un Point2d xymais se référer au Xchamp de ce champ comme étant soit xy.Xou X.
supercat
8

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 structhé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.

Blixt
la source
4
Ce n'est pas la raison pour laquelle il n'y a pas d'héritage.
Dykam
Je crois que l'héritage dont il est question ici n'est pas de pouvoir utiliser deux structures où l'une hérite de l'autre de manière interchangeable, mais de réutiliser et d'ajouter à l'implémentation d'une structure à une autre (c'est-à-dire créer un à Point3Dpartir de a Point2D; vous ne seriez pas capable d'utiliser a Point3Dau lieu de a Point2D, mais vous n'auriez pas à réimplémenter Point3Dentièrement le tout à partir de zéro.) C'est ainsi que je l'ai interprété de toute façon ...
Blixt
En bref: il pourrait prendre en charge l'héritage sans polymorphisme. Ce n'est pas le cas. Je pense que c'est un choix de conception pour aider une personne à choisir classle structcas échéant.
Blixt
3

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.

Dykam
la source
2
C ++ gère ce cas très bien, IIRC. L'instance de B est découpée pour s'adapter à la taille d'un A. S'il s'agit d'un type de données pur, comme le sont les structures .NET, alors rien de mauvais ne se produira. Vous rencontrez un problème avec une méthode qui renvoie un A et vous stockez cette valeur de retour dans un B, mais cela ne devrait pas être autorisé. En bref, les concepteurs .NET auraient pu gérer cela s'ils le voulaient, mais ils ne l'ont pas fait pour une raison quelconque.
rmeador
1
Pour votre DoSomething (), il n'y aura probablement pas de problème car (en supposant la sémantique C ++) 'b' serait "découpé" pour créer une instance A. Le problème vient des <i> tableaux </i>. Considérez vos structures A et B existantes, ainsi qu'une méthode <c> DoSomething (A [] arg) {arg [1] .property = 1;} </c>. Étant donné que les tableaux de types valeur stockent les valeurs "en ligne", DoSomething (réel = nouveau B [2] {}) provoquera la définition de [0] .childproperty réelle, et non de réelle [1] .property. C'est mauvais.
jonp
2
@John: Je n'affirmais pas que c'était le cas, et je ne pense pas que @jonp l'était non plus. Nous mentionnions simplement que ce problème est ancien et a été résolu, de sorte que les concepteurs de .NET ont choisi de ne pas le prendre en charge pour une raison autre que l'infaisabilité technique.
rmeador
Il convient de noter que le problème des «tableaux de types dérivés» n'est pas nouveau pour C ++; voir parashift.com/c++-faq-lite/proper-inheritance.html#faq-21.4 (Les tableaux en C ++ sont mauvais! ;-)
jonp
@John: la solution au problème "les tableaux de types dérivés et de types de base ne se mélangent pas" est, comme d'habitude, de ne pas faire ça. C'est pourquoi les tableaux en C ++ sont mauvais (permet plus facilement la corruption de la mémoire), et pourquoi .NET ne prend pas en charge l'héritage avec les types valeur (le compilateur et JIT s'assurent que cela ne peut pas se produire).
jonp
3

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. :-)

Rui Craveiro
la source
3

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.

user38001
la source
1
Presque. Personne d'autre n'a mentionné le problème de découpage en référence à la pile, uniquement en référence aux tableaux. Et personne d'autre n'a mentionné les solutions disponibles.
user38001
1
Tous les types de valeur dans .net sont remplis de zéro lors de la création, quels que soient leur type ou les champs qu'ils contiennent. Ajouter quelque chose comme un pointeur vtable à une structure nécessiterait un moyen d'initialiser les types avec des valeurs par défaut non nulles. Une telle fonctionnalité peut être utile à diverses fins, et l'implémentation d'une telle chose dans la plupart des cas peut ne pas être trop difficile, mais rien de proche n'existe dans .net.
supercat
@ user38001 "Les structures sont allouées sur la pile" - sauf s'il s'agit de champs d'instance auquel cas elles sont allouées sur le tas.
David Klempfner
2

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.

Cecil a un nom
la source
Le compilateur sait ce qu'il y a. Faire référence à C ++, cela ne peut pas être la réponse.
Henk Holterman
D'où avez-vous déduit le C ++? Je dirais en place parce que c'est ce qui correspond le mieux au comportement, la pile est un détail d'implémentation, pour citer un article de blog MSDN.
Cecil a un nom
Oui, mentionner C ++ était mauvais, juste ma pensée. Mais à part la question de savoir si les informations d'exécution sont nécessaires, pourquoi les structures ne devraient-elles pas avoir un `` en-tête d'objet ''? Le compilateur peut les écraser comme bon lui semble. Il pourrait même masquer un en-tête sur une structure [Structlayout].
Henk Holterman
Étant donné que les structs sont des types valeur, cela n'est pas nécessaire avec un en-tête d'objet car l'exécution copie toujours le contenu comme pour les autres types valeur (une contrainte). Cela n'aurait pas de sens avec un en-tête, car c'est à cela que servent les classes de type référence: P
Cecil a un nom
1

Les structures prennent en charge les interfaces, vous pouvez donc faire certaines choses polymorphes de cette façon.

Wyatt Barnett
la source
0

IL est un langage basé sur la pile, donc appeler une méthode avec un argument ressemble à ceci:

  1. Poussez l'argument sur la pile
  2. Appelez la méthode.

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.

Matt Howells
la source
1
C ++ a résolu cela, lisez cette réponse pour le vrai problème: stackoverflow.com/questions/1222935
...