L'utilisation de " new " sur une structure l'alloue-t-elle sur le tas ou la pile?
Lorsque vous créez une instance d'une classe avec l'opérateur new
, la mémoire est allouée sur le tas. Lorsque vous créez une instance d'une structure avec les new
exploitant de la mémoire sont alloués sur le tas ou sur la pile ?
8 réponses
OK, voyons si je peux rendre ça plus clair.
Tout d'abord, Ash a raison: la question Est Pas sur l'endroit où le type de valeur variables sont alloués. C'est une question différente - et à laquelle la réponse n'est pas seulement "sur la pile". C'est plus compliqué que cela (et rendu encore plus compliqué par C# 2). J'ai un article sur le sujet et je vais le développer si demandé, mais traitons juste de l'opérateur new
.
Deuxièmement, tous les cela dépend vraiment 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 termes d'optimisation de beaucoup d'allocation "logique".
Troisièmement, j'ignore les génériques, surtout parce que je ne connais pas vraiment la réponse, et en partie parce que cela compliquerait trop les choses.
Enfin, tout cela est juste avec le courant application. La spécification C# ne spécifie pas beaucoup de cela-c'est effectivement un détail d'implémentation. Il y a ceux qui croient que les développeurs de code géré ne devraient vraiment pas se soucier. Je ne suis pas sûr d'aller aussi loin, mais il vaut la peine d'imaginer un monde où en fait toutes les variables locales vivent sur le tas - ce qui serait toujours conforme à la spécification.
Il existe deux situations différentes avec l'opérateur new
sur les types de valeurs: vous pouvez appeler un constructeur sans paramètre (par exemple new Guid()
) ou un constructeur paramétrable (par exemple new Guid(someString)
). Ceux-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 valeurs ont un constructeur sans paramètre. Selon la spécification CLI, Aucun Type de valeur n'a de constructeurs sans paramètre. (Récupérez les constructeurs d'un type de valeur avec réflexion un certain temps - vous n'en trouverez pas un sans paramètre.)
Il est logique que C# traite "initialiser une valeur avec des zéros" comme un constructeur, car il maintient le langage cohérent - vous pouvez penser à new(...)
comme toujours appelant un constructeur. Il est logique pour la CLI de penser différemment, car il n'y a pas de code réel à appeler - et certainement pas de code spécifique au type.
Cela fait également une différence entre ce que vous allez faire avec la valeur après l'avoir initialisée. L'IL utilisé pour
Guid localVariable = new Guid(someString);
Est différent de L'IL utilisé pour:
myInstanceOrStaticVariable = new Guid(someString);
En outre, si la valeur est utilisée comme valeur intermédiaire, par exemple un argument à un appel de méthode, les choses sont légèrement différentes à nouveau. 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érerait 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.ToString();
}
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.ToString();
}
static void ParameterlessCtorCallMethod()
{
MethodTakingGuid(new Guid());
}
}
Voici L'IL pour la classe, à l'exclusion des bits non pertinents (tels que 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(string)
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(string)
// Removed ToString() 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(string)
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 ToString() 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, il y a beaucoup d'instructions différentes utilisées pour appeler le constructeur:
-
newobj
: alloue la valeur sur la pile, appelle un constructeur paramétré. Utilisé pour les valeurs intermédiaires, par exemple pour l'affectation à un champ ou l'utilisation comme argument de méthode. -
call instance
: utilise un emplacement de stockage déjà alloué (que ce soit sur la pile ou non). Il est utilisé dans le code ci-dessus pour l'affectation à une variable locale. Si la même variable locale se voit attribuer une valeur plusieurs fois à l'aide de plusieurs appelsnew
, elle initialise simplement les données au - dessus du ancienne valeur - elle n'alloue pas plus d'espace de pile à 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 parinitobj
.
J'espère que cela montre à quel point le sujet est compliqué, tout en y mettant un peu de lumière en même temps. Dans certains sens conceptuels , chaque appel à new
alloue de l'espace sur la pile - mais comme nous l'avons vu, ce n'est pas ce qui se passe vraiment, même au niveau IL. J'aimerais attirer votre attention sur un cas particulier. Prenez cette méthode:
void HowManyStackAllocations()
{
Guid guid = new Guid();
// [...] Use guid
guid = new Guid(someBytes);
// [...] Use guid
guid = new Guid(someString);
// [...] Use guid
}
Cela" logiquement " a 4 allocations de pile - une pour la variable, et une pour chacun des trois appels new
- mais en fait (pour ce code spécifique) la pile n'est allouée qu'une seule fois, puis le même emplacement de stockage est réutilisé.
EDIT: juste pour être clair, cela 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, c'est pourquoi le compilateur C# est capable de réutiliser le même emplacement de pile. Voir le blog D'Eric Lippert sur la construction de type de valeur pour plus de détails et un cas où ne s'applique pas à.
J'ai beaucoup appris en écrivant cette réponse - veuillez demander des éclaircissements si cela n'est pas clair!
La mémoire contenant les champs d'une structure peut être allouée sur la pile ou le tas en fonction des circonstances. Si la variable struct-type est une variable ou un paramètre local qui n'est pas capturé par un délégué anonyme ou une classe d'itérateur, elle sera allouée sur la pile. Si la variable fait partie d'une classe, elle sera allouée dans la classe sur le 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 de la mémoire. Le seul but serait de définir les valeurs de champ en fonction de ce qui est 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 pile, sauf que C # nécessite que toutes les variables locales soient définies sur une valeur 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 les structures).
Pour le dire de manière compacte, new est un terme impropre pour les structures, l'appel de new appelle simplement le constructeur. Le seul emplacement de stockage pour la structure est l'emplacement défini.
Si c'est un membre de la variable est stockée directement dans quoi que ce soit définie, si c'est une variable locale ou un paramètre, il est stocké sur la pile.
Contraste ceci avec les classes, qui ont une référence partout où la structure aurait été stockée dans son intégralité, tandis que les points de référence quelque part sur le tas. (Membre à l'intérieur, local / paramètre sur la pile)
Cela peut aider à regarder un peu en C++, où il n'y a pas de véritable distinction entre class/struct. (Il y a des noms similaires dans la langue, mais ils ne font référence qu'à l'accessibilité par défaut des choses) lorsque vous appelez new, vous obtenez un pointeur vers l'emplacement du tas, alors que si vous avez une référence non-pointeur, elle est stockée directement sur la pile ou dans l'autre objet, Ala structs en C#.
Comme pour tous les types de valeurs, les structures vont toujours là où elles étaient 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 structures.
Edit: j'avais mistankely répondu qu'ilsToujours vont dans la pile. C'est incorrect.
Je manque probablement quelque chose ici, mais pourquoi nous soucions-nous de l'allocation?
Les types de valeur sont passés par value;) et ne peuvent donc pas être mutés à une portée différente de celle où ils sont définis. Pour pouvoir muter la valeur, vous devez ajouter le mot-clé [ref].
Les types de référence sont passés par référence et peuvent être mutés.
Il y a bien sûr des types de référence immuables les chaînes étant les plus populaires.
DISPOSITION/initialisation du tableau: Types de valeur - > zéro mémoire [nom,code postal][nom,zip] Types de référence - > zéro mémoire - > null [ref] [ref]
Une déclaration class
ou struct
est comme un blueprint utilisé pour créer des instances ou des objets au moment de l'exécution. Si vous définissez un class
ou struct
appelé Person, Person est le nom du type. Si vous déclarez et initialisez une variable P de type Person, p est considéré comme un objet 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 fields
.
Un class
est un type de référence. Quand un objet du class
est créé, la variable à laquelle l'objet est affecté ne contient qu'une référence à cette mémoire. Lorsque la référence d'objet est affectée à une nouvelle variable, la nouvelle variable fait référence à l'objet d'origine. Les modifications apportées par le biais d'une variable sont reflétées dans l'autre variable parce qu'ils se réfèrent aux mêmes données.
Un struct
est un type valeur. Lorsqu'un struct
est créé, la variable à laquelle le struct
est affecté contient les données réelles de la structure. Lorsque struct
est affecté à une nouvelle variable, il est copié. La nouvelle variable et la variable originale contiennent donc deux copies distinctes des mêmes données. Les modifications apportées à une copie n'affectent pas l'autre copie.
En général, classes
sont utilisés pour modéliser un comportement plus complexe ou des données destinées à être modifiées après la création d'un objet class
. Structs
sont les mieux adaptés pour les petites structures de données qui contiennent principalement des données qui ne sont pas destinées à être modifiées après la création de struct
.
À peu près les structures qui sont considérées comme des types de valeur, sont allouées sur la pile, tandis que les objets sont alloués sur le tas, tandis que la référence d'objet (pointeur) est allouée sur la pile.
Les structures sont allouées à la pile. Voici une explication utile:
En outre, les classes instanciées dans. NET allouent de la mémoire sur le tas ou l'espace mémoire réservé de. Net. Alors que les structures donnent plus efficacité lors de l'instanciation en raison de l'allocation sur la pile. En outre, il convient de noter que le passage de paramètres dans les structures sont fait par valeur.