Lorsque vous créez une instance d'une classe avec l' new
opérateur, la mémoire est allouée sur le tas. Lorsque vous créez une instance d'une structure avec l' new
opérateur où la mémoire est-elle allouée, sur le tas ou sur la pile?
Lorsque vous créez une instance d'une classe avec l' new
opérateur, la mémoire est allouée sur le tas. Lorsque vous créez une instance d'une structure avec l' new
opérateur où la mémoire est-elle allouée, sur le tas ou sur la pile?
Réponses:
D'accord, 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 seulement "sur la pile". 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 si demandé, mais traitons simplementnew
opérateur.
Deuxièmement, tout cela dépend vraiment du niveau dont 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, principalement parce que je ne connais pas vraiment la réponse, et en partie parce que cela compliquerait trop les choses.
Enfin, tout cela ne concerne que l'implémentation actuelle. La spécification C # ne spécifie pas grand-chose - c'est en fait un détail d'implémentation. Il y a ceux qui pensent que les développeurs de code managé ne devraient vraiment pas s'en soucier. Je ne suis pas sûr que j'irais aussi loin, mais cela 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' new
opérateur sur les types de valeur: vous pouvez appeler un constructeur sans paramètre (par exemple new Guid()
) ou un constructeur avec paramètre (par exemple new Guid(someString)
). Ceux-ci génèrent une IL significativement différente. 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 réflexion un certain temps - vous n'en trouverez pas un sans paramètre.)
Il est logique que C # traite le "initialiser une valeur avec des zéros" comme un constructeur, car il maintient la cohérence du langage - vous pouvez penser new(...)
comme toujours appeler un constructeur. Il est logique que la CLI pense différemment, car il n'y a pas de vrai code à appeler - et certainement pas de code spécifique au type.
Cela fait également une différence 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);
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 à nouveau légèrement différentes. Pour montrer toutes ces différences, voici un petit programme de test. Il ne montre pas la différence entre les variables statiques et les variables d'instance: l'IL serait différent 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 de 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, de nombreuses instructions différentes sont 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 comme argument de méthode.call instance
: Utilise un emplacement de stockage déjà alloué (sur la pile ou non). Ceci est utilisé dans le code ci-dessus pour l'affectation à une variable locale. Si la même variable locale se voit attribuer plusieurs fois une valeur à l'aide de plusieurs new
appels, elle initialise simplement les données au-dessus de l'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 sont affectés à 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 mettant un peu de lumière dessus en même temps. Dans certains sens conceptuels, chaque appel à new
allouer de l'espace sur la pile - mais comme nous l'avons vu, ce n'est pas vraiment ce qui se passe même au niveau IL. Je voudrais 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(someString);
// [...] Use guid
}
Ce "logiquement" a 4 allocations de pile - une pour la variable et une pour chacun des trois new
appels - 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 Guid
constructeur 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 types de valeur pour plus de détails et un cas où cela ne s'applique pas .
J'ai beaucoup appris en écrivant cette réponse - veuillez demander des éclaircissements si l'un d'eux n'est pas clair!
List<Guid>
et ajoutez-les? Ce serait 3 allocations (même IL)? Mais ils sont gardés dans un endroit magique
guid
n'a été qu'à moitié écrasée, car elle ne sera pas visible de toute façon.
La mémoire contenant les champs d'une structure peut être allouée sur la pile ou le tas selon les circonstances. Si la variable de type struct est une variable ou un paramètre local qui n'est pas capturé par une classe de délégué ou d'itérateur anonyme, alors il sera alloué sur la pile. Si la variable fait partie d'une classe, elle sera allouée au sein de 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 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 pile, sauf que C # nécessite que toutes les variables locales soient définies sur une certaine valeur avant d'être utilisées, vous devez donc appeler soit un constructeur personnalisé soit le constructeur par défaut (un constructeur qui ne prend aucun paramètre est toujours disponible pour structures).
Pour le dire de façon compacte, new est un terme impropre pour les structures, appeler new appelle simplement le constructeur. Le seul emplacement de stockage pour la structure est l'emplacement où elle est définie.
S'il s'agit d'une variable membre, elle est stockée directement dans ce qu'elle est définie, si c'est une variable ou un paramètre local, elle est stockée dans la pile.
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 à l'intérieur, local / paramètre sur la pile)
Il peut être utile de regarder un peu en C ++, où il n'y a pas de réelle distinction entre classe / struct. (Il existe des noms similaires dans la langue, mais ils se réfèrent uniquement à l'accessibilité par défaut des choses) Lorsque vous appelez new, vous obtenez un pointeur vers l'emplacement du tas, tandis que si vous avez une référence sans pointeur, elle est stockée directement sur la pile ou dans l'autre objet, ala structs en C #.
Comme avec tous les types de valeurs, 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 structures.
Edit: j'avais par erreur répondu qu'ils vont TOUJOURS dans la pile. C'est incorrect .
Il me manque probablement quelque chose ici, mais pourquoi nous soucions-nous de l'allocation?
Les types de valeur sont passés par valeur;) 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 transmis par référence et peuvent être mutés.
Il existe bien sûr des chaînes de types de référence immuables qui sont les plus populaires.
Présentation / initialisation du tableau: types de valeurs -> mémoire zéro [nom, zip] [nom, zip] Types de référence -> mémoire zéro -> null [réf] [réf]
Une déclaration class
or struct
est comme un plan directeur utilisé pour créer des instances ou des objets au moment de l'exécution. Si vous définissez une class
ou struct
appelée Personne, Personne est le nom du type. Si vous déclarez et initialisez une variable p de type Person, p est dit être 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
.
A class
est un type de référence. Lorsqu'un objet de 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 via une variable sont reflétées dans l'autre variable car elles font toutes deux référence aux mêmes données.
A struct
est un type de valeur. Lors de la struct
création de a, la variable à laquelle struct
est affecté contient les données réelles de la structure. Lorsque le struct
est affecté à une nouvelle variable, il est copié. La nouvelle variable et la variable d'origine 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, ils classes
sont utilisés pour modéliser des comportements plus complexes ou des données destinées à être modifiées après la class
création d' un objet. Structs
conviennent mieux aux petites structures de données qui contiennent principalement des données qui ne sont pas destinées à être modifiées après leur struct
création.
À 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, lorsqu'elles sont instanciées dans .NET, les classes allouent de la mémoire sur le tas ou l'espace mémoire réservé de .NET. Alors que les structures donnent plus d'efficacité lorsqu'elles sont instanciées en raison de l'allocation sur la pile. En outre, il convient de noter que les paramètres de passage dans les structures se font par valeur.