web-dev-qa-db-fra.com

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 l'opérateur new, où la mémoire est-elle allouée, sur le tas ou sur la pile?

277
kedar kamthe

Bon, voyons si je peux clarifier cela.

Tout d’abord, Ash a raison: la question est et non sur le type de valeur des variables sont attribués. C'est une question différente - et à laquelle la réponse n'est pas simplement "sur la pile". C'est plus compliqué que cela (et rendu encore plus compliqué par le C # 2). J'ai un article sur le sujet et le développerai si demandé, mais parlons simplement de l'opérateur new.

Deuxièmement, tout dépend du niveau dont vous parlez. Je regarde ce que le compilateur fait avec le code source, en termes de l’IL qu’il crée. Il est plus que possible que le compilateur JIT fasse des choses intelligentes en optimisant beaucoup d'allocation "logique".

Troisièmement, j'ignore les génériques, principalement 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 spécifie pas grand chose, c'est en réalité un détail d'implémentation. Il y a ceux qui croient que les développeurs de code géré ne devraient vraiment pas s'en soucier. Je ne suis pas sûr que j'irais aussi loin, mais il vaut la peine d'imaginer un monde dans lequel 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 valeur: vous pouvez appeler un constructeur sans paramètre (par exemple new Guid()) ou un constructeur paramétré (par exemple new Guid(someString)). Ceux-ci génèrent des IL significativement différentes. 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, les types de valeur no ont des constructeurs sans paramètre. (Récupérez les constructeurs d'un type de valeur avec une réflexion dans le temps - vous ne trouverez pas de constructeur sans paramètre.)

Il est logique que C # considère "initialiser une valeur avec des zéros" comme un constructeur, car il garde le langage cohérent - vous pouvez penser à new(...) comme à toujours appeler un constructeur. Il est logique que la CLI y pense différemment, car il n’ya pas de code réel à appeler - et certainement pas de code spécifique au type.

Cela fait également une différence sur ce que vous allez faire avec la valeur après l'avoir initialisée. Le 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 ex. 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, en excluant les 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 constater, 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 utiliser 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 assigner une variable locale. Si une même valeur locale est affectée plusieurs fois à une valeur à l’aide de plusieurs appels new, elle initialise simplement les données par-dessus l’ancienne valeur - elle ne le fait pas alloue plus d'espace de pile à chaque fois.
  • initobj: Utilise un emplacement de stockage déjà alloué et nettoie 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 effacée par initobj.

J'espère que cela montre à quel point le sujet est compliqué, tout en y apportant un peu de lumière. 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 que arrive vraiment 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 appels new - mais en fait (pour ce code spécifique) la pile n'est allouée qu'une seule fois, puis le même stockage l'emplacement est réutilisé.

EDIT: Juste pour préciser, 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 # peut réutiliser le même emplacement de pile. Voir le commentaire d'Eric Lippert billet de blog 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 elle n'est pas claire!

298
Jon Skeet

La mémoire contenant les champs d'une structure peut être allouée sur la pile ou sur 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, elle sera allouée sur la pile. Si la variable fait partie d'une classe, elle sera allouée à l'intérieur 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 auront leurs valeurs par défaut (0 ou null).

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

38

Pour résumer, new est un terme impropre pour structs, appeler new appelle simplement le constructeur. Le seul emplacement de stockage de la structure est l'emplacement où il est défini.

S'il s'agit d'une variable membre, il est stocké directement dans la définition dans laquelle il est défini. S'il s'agit d'une variable ou d'un paramètre local, il est stocké dans la pile.

Cela contraste avec les 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 au sein, 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 class/struct. (Il existe 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 segment de mémoire, tandis 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 #.

12
Guvante

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

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

Edit: J'avais répondu par erreur qu'ils [~ # ~] toujours [~ # ~] vont dans la pile . C'est incorrect .

5
Esteban Araya

Il me manque probablement quelque chose ici, mais pourquoi nous intéressons-nous à l'allocation?

Les types de valeur sont passés par valeur;) et ne peuvent donc pas être mutés dans une portée différente de celle où ils ont été 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 qui sont les plus populaires.

Disposition/initialisation du tableau: types de valeur -> zéro mémoire [nom, Zip] [nom, Zip] Types de référence -> zéro mémoire -> null [ref] [ref]

4
user18579

Une déclaration class ou struct ressemble à un plan utilisé pour créer des instances ou des objets au moment de l'exécution. Si vous définissez un class ou struct appelé Personne, Personne 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. 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 à l'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.

Un struct est un type de valeur. Lorsqu'un struct est créé, la variable à laquelle est assigné le struct contient les données réelles de la structure. Lorsque struct est affecté à une nouvelle variable, celle-ci est copiée. 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, classes permet de modéliser des comportements plus complexes ou des données destinées à être modifiées après la création d'un objet class. Structs conviennent particulièrement aux petites structures de données contenant principalement des données qu'il n'est pas prévu de modifier après la création de struct.

pour plus ...

2
Sujit

La plupart des structures considérées comme des types Value sont allouées sur une pile, tandis que les objets sont alloués sur un tas, tandis que la référence à l'objet (pointeur) est allouée sur la pile.

1
bashmohandes

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

Structs

En outre, lorsqu'elles sont instanciées dans .NET, les classes allouent de la mémoire sur le tas ou dans l'espace mémoire réservé de .NET. Considérant que les structures donnent plus d’efficacité lorsqu’elles sont instanciées en raison de l’allocation sur la pile. De plus, il convient de noter que les paramètres de passage dans les structures sont effectués par valeur.

1
DaveK