L’utilisation de «new» sur une structure l’alloue-t-elle sur le tas ou la stack?

Lorsque vous créez une instance d’une classe avec le new opérateur, la mémoire est allouée sur le tas. Lorsque vous créez une instance d’une structure avec le new opérateur où la mémoire est-elle allouée, sur le tas ou sur la stack?

Ok, voyons si je peux rendre cela plus clair.

Tout d’abord, Ash a raison: la question n’est pas de savoir où les variables de type valeur sont allouées. C’est une question différente – et à laquelle la réponse n’est pas simplement “sur la stack”. C’est plus compliqué que ça (et rendu encore plus compliqué par C # 2). J’ai un article sur le sujet et je le développerai sur demande, mais traitons uniquement le new opérateur.

Deuxièmement, tout dépend de quel niveau vous parlez. Je regarde ce que le compilateur fait avec le code source, en termes d’IL qu’il crée. Il est plus que possible que le compilateur JIT fasse des choses intelligentes en optimisant une grande partie des allocations “logiques”.

Troisièmement, j’ignore les génériques, surtout parce que je ne connais pas la réponse, et en partie parce que cela compliquerait trop les choses.

Enfin, tout cela ne concerne que la mise en œuvre actuelle. La spécification C # ne précise pas grand chose – c’est effectivement un détail d’implémentation. Il y a ceux qui croient que les développeurs de code managé ne devraient vraiment pas s’en soucier. Je ne suis pas sûr d’aller aussi loin, mais cela vaut la peine d’imaginer un monde où toutes les variables locales vivent sur le tas – ce qui serait toujours conforme aux spécifications.


Il y a deux situations différentes avec le new opérateur sur les types de valeur: vous pouvez appeler un constructeur sans paramètre (par exemple, new Guid() ) ou un constructeur paramétrable (par exemple, new Guid(someSsortingng) ). Celles-ci génèrent des IL significativement différents. Pour comprendre pourquoi, vous devez comparer les spécifications C # et CLI: selon C #, tous les types de valeur ont un constructeur sans paramètre. Selon la spécification CLI, aucun type de valeur n’a de constructeur sans paramètre. (Récupérez les constructeurs d’un type de valeur avec reflection à un moment donné – vous ne trouverez pas de paramètre sans paramètre.)

Il est logique que C # traite le paramètre “initialize a zeroes” comme un constructeur, car il conserve la cohérence du langage – vous pouvez penser à new(...) comme appelant toujours un constructeur. Il est logique que la CLI pense différemment, car il n’y a pas de code réel à appeler – et certainement pas de code spécifique au type.

Cela fait aussi une différence ce que vous allez faire avec la valeur après l’avoir initialisée. Le IL utilisé pour

 Guid localVariable = new Guid(someSsortingng); 

est différent de celui utilisé pour:

 myInstanceOrStaticVariable = new Guid(someSsortingng); 

De plus, si la valeur est utilisée comme valeur intermédiaire, par exemple un argument pour un appel de méthode, les choses sont légèrement différentes. Pour montrer toutes ces différences, voici un programme de test court. Il ne montre pas la différence entre les variables statiques et les variables d’instance: l’IL diffère entre stfld et stsfld , mais c’est tout.

 using System; public class Test { static Guid field; static void Main() {} static void MethodTakingGuid(Guid guid) {} static void ParameterisedCtorAssignToField() { field = new Guid(""); } static void ParameterisedCtorAssignToLocal() { Guid local = new Guid(""); // Force the value to be used local.ToSsortingng(); } static void ParameterisedCtorCallMethod() { MethodTakingGuid(new Guid("")); } static void ParameterlessCtorAssignToField() { field = new Guid(); } static void ParameterlessCtorAssignToLocal() { Guid local = new Guid(); // Force the value to be used local.ToSsortingng(); } static void ParameterlessCtorCallMethod() { MethodTakingGuid(new Guid()); } } 

Voici l’IL pour la classe, à l’exclusion des bits non pertinents (tels que les nops):

 .class public auto ansi beforefieldinit Test extends [mscorlib]System.Object { // Removed Test's constructor, Main, and MethodTakingGuid. .method private hidebysig static void ParameterisedCtorAssignToField() cil managed { .maxstack 8 L_0001: ldstr "" L_0006: newobj instance void [mscorlib]System.Guid::.ctor(ssortingng) L_000b: stsfld valuetype [mscorlib]System.Guid Test::field L_0010: ret } .method private hidebysig static void ParameterisedCtorAssignToLocal() cil managed { .maxstack 2 .locals init ([0] valuetype [mscorlib]System.Guid guid) L_0001: ldloca.s guid L_0003: ldstr "" L_0008: call instance void [mscorlib]System.Guid::.ctor(ssortingng) // Removed ToSsortingng() call L_001c: ret } .method private hidebysig static void ParameterisedCtorCallMethod() cil managed { .maxstack 8 L_0001: ldstr "" L_0006: newobj instance void [mscorlib]System.Guid::.ctor(ssortingng) L_000b: call void Test::MethodTakingGuid(valuetype [mscorlib]System.Guid) L_0011: ret } .method private hidebysig static void ParameterlessCtorAssignToField() cil managed { .maxstack 8 L_0001: ldsflda valuetype [mscorlib]System.Guid Test::field L_0006: initobj [mscorlib]System.Guid L_000c: ret } .method private hidebysig static void ParameterlessCtorAssignToLocal() cil managed { .maxstack 1 .locals init ([0] valuetype [mscorlib]System.Guid guid) L_0001: ldloca.s guid L_0003: initobj [mscorlib]System.Guid // Removed ToSsortingng() call L_0017: ret } .method private hidebysig static void ParameterlessCtorCallMethod() cil managed { .maxstack 1 .locals init ([0] valuetype [mscorlib]System.Guid guid) L_0001: ldloca.s guid L_0003: initobj [mscorlib]System.Guid L_0009: ldloc.0 L_000a: call void Test::MethodTakingGuid(valuetype [mscorlib]System.Guid) L_0010: ret } .field private static valuetype [mscorlib]System.Guid field } 

Comme vous pouvez le voir, de nombreuses instructions différentes sont utilisées pour appeler le constructeur:

  • newobj : newobj la valeur sur la stack, appelle un constructeur paramétré. Utilisé pour les valeurs intermédiaires, par exemple pour l’affectation à un champ ou comme argument de méthode.
  • call instance : Utilise un emplacement de stockage déjà alloué (que ce soit sur la stack ou non). Ceci est utilisé dans le code ci-dessus pour l’affectation à une variable locale. Si la même variable locale se voit atsortingbuer une valeur plusieurs fois en utilisant plusieurs new appels, elle initialise simplement les données au-dessus de l’ancienne valeur – elle n’alloue pas plus d’espace de stack à chaque fois.
  • initobj : Utilise un emplacement de stockage déjà alloué et efface simplement les données. Ceci est utilisé pour tous nos appels de constructeur sans paramètre, y compris ceux qui affectent une variable locale. Pour l’appel de méthode, une variable locale intermédiaire est effectivement introduite et sa valeur est effacée par initobj .

J’espère que cela montre à quel point le sujet est compliqué, tout en faisant briller un peu de lumière. Dans certains sens conceptuels, chaque appel à un new espace alloue de la place sur la stack – mais comme nous l’avons vu, ce n’est pas ce qui se passe réellement au niveau de l’IL. J’aimerais souligner un cas particulier. Prenez cette méthode:

 void HowManyStackAllocations() { Guid guid = new Guid(); // [...] Use guid guid = new Guid(someBytes); // [...] Use guid guid = new Guid(someSsortingng); // [...] Use guid } 

Cela “logiquement” a 4 allocations de stack – une pour la variable et une pour chacun des trois new appels – mais en fait (pour ce code spécifique) la stack n’est allouée qu’une seule fois, puis le même emplacement de stockage est réutilisé.

EDIT: Juste pour être clair, ce n’est vrai que dans certains cas … en particulier, la valeur de guid ne sera pas visible si le constructeur Guid lève une exception, ce qui explique pourquoi le compilateur C # est capable de réutiliser la même stack fente. Voir l’article de Eric Lippert sur la construction de type valeur pour plus de détails et un cas où il ne s’applique pas .

J’ai beaucoup appris en écrivant cette réponse – demandez s’il vous plaît des éclaircissements si l’un d’entre eux n’est pas clair!

La mémoire contenant les champs d’une structure peut être allouée sur la stack ou sur le tas selon les circonstances. Si la variable struct-type est une variable locale ou un paramètre qui n’est pas capturé par une classe de délégué ou d’iterator anonyme, elle sera alors allouée sur la stack. Si la variable fait partie d’une classe, elle sera allouée dans la classe du tas.

Si la structure est allouée sur le tas, l’appel du nouvel opérateur n’est pas réellement nécessaire pour allouer la mémoire. Le seul but serait de définir les valeurs de champ en fonction de ce qui se trouve dans le constructeur. Si le constructeur n’est pas appelé, tous les champs obtiendront leurs valeurs par défaut (0 ou null).

De même, pour les structures allouées sur la stack, sauf que C # exige que toutes les variables locales soient définies avant d’être utilisées, vous devez donc appeler un constructeur personnalisé ou le constructeur par défaut (un constructeur qui ne prend aucun paramètre est toujours disponible pour structs).

Pour le dire de manière compacte, new est un abus de langage pour structs, l’appel new appelle simplement le constructeur. Le seul emplacement de stockage pour la structure est l’emplacement défini.

S’il s’agit d’une variable membre, celle-ci est stockée directement dans tout ce qui est défini, s’il s’agit d’une variable locale ou d’un paramètre stocké dans la stack.

Comparez cela aux classes, qui ont une référence partout où la structure aurait été stockée dans son intégralité, tandis que la référence pointe quelque part sur le tas. (Membre dans, local / paramètre sur la stack)

Il peut être utile de regarder un peu en C ++, où il n’ya pas de réelle distinction entre class / struct. (Il y a des noms similaires dans le langage, mais ils ne font référence qu’à l’accessibilité par défaut des choses) Lorsque vous appelez new, vous obtenez un pointeur sur l’emplacement du tas, alors que si vous avez une référence non-pointeur dans l’autre object, ala structs en C #.

Comme pour tous les types de valeur, les structures vont toujours là où elles ont été déclarées .

Voir cette question ici pour plus de détails sur l’utilisation des structures. Et cette question ici pour plus d’informations sur les structs.

Edit: J’avais mal répondu qu’ils allaient TOUJOURS dans la stack. Ceci est incorrect

Je manque probablement quelque chose ici, mais pourquoi est-ce que nous nous soucions de la répartition?

Les types de valeur sont passés par valeur;) et ne peuvent donc pas être mutés dans une étendue différente de celle où ils sont définis. Pour pouvoir modifier la valeur, vous devez append le mot-clé [ref].

Les types de référence sont transmis par référence et peuvent être mutés.

Il existe bien sûr des types de référence immuables, les plus populaires.

Disposition / initialisation des tableaux: types de valeurs -> zéro mémoire [nom, zip] [nom, zip] types de référence -> zéro mémoire -> null [ref] [ref]

Une déclaration de class ou de struct est comme un modèle utilisé pour créer des instances ou des objects au moment de l’exécution. Si vous définissez une class ou une struct appelée Person, Person est le nom du type. Si vous déclarez et initialisez une variable de type Person, on dit que p est un object ou une instance de Person. Plusieurs instances du même type de personne peuvent être créées et chaque instance peut avoir des valeurs différentes dans ses properties et ses fields .

Une class est un type de référence. Lorsqu’un object de la class est créé, la variable à laquelle l’object est affecté ne contient qu’une référence à cette mémoire. Lorsque la référence d’object est affectée à une nouvelle variable, la nouvelle variable fait référence à l’object d’origine. Les modifications apscopes à une variable sont reflétées dans l’autre variable car elles se réfèrent toutes deux aux mêmes données.

Une struct est un type de valeur. Lorsqu’une struct est créée, la variable à laquelle la struct est affectée contient les données réelles de la structure. Lorsque la struct est affectée à une nouvelle variable, elle est copiée. La nouvelle variable et la variable d’origine contiennent donc deux copies distinctes des mêmes données. Les modifications apscopes à une copie n’affectent pas l’autre copie.

En général, les classes sont utilisées pour modéliser un comportement plus complexe ou des données destinées à être modifiées après la création d’un object de class . Structs sont mieux adaptées aux petites structures de données contenant principalement des données qui ne sont pas destinées à être modifiées après la création de la struct .

pour plus…

A peu près les structures qui sont considérées comme des types de valeur, sont allouées sur la stack, tandis que les objects sont alloués sur le tas, tandis que la référence d’object (le pointeur) est allouée sur la stack.

Les structures sont allouées à la stack. Voici une explication utile:

Structs

En outre, les classes instanciées dans .NET allouent de la mémoire sur le segment de mémoire ou sur l’espace mémoire réservé de .NET. Alors que les structs produisent plus d’efficacité quand ils sont instanciés en raison d’une allocation sur la stack. En outre, il convient de noter que les parameters de passage dans les structures sont définis par valeur.