L'uso di "nuovo" su una struttura lo alloca sull'heap o sullo stack?


290

Quando si crea un'istanza di una classe con l' newoperatore, la memoria viene allocata sull'heap. Quando si crea un'istanza di una struttura con l' newoperatore da dove viene allocata la memoria, sull'heap o sullo stack?

Risposte:


305

Bene, vediamo se riesco a chiarire questo aspetto.

In primo luogo, Ash ha ragione: la domanda non riguarda dove sono allocate le variabili del tipo di valore . Questa è una domanda diversa - e una a cui la risposta non è solo "in pila". È più complicato di così (e reso ancora più complicato da C # 2). Ho un articolo sull'argomento e, se richiesto, lo espanderò, ma ci occupiamo solo newdell'operatore.

In secondo luogo, tutto ciò dipende davvero dal livello di cui stai parlando. Sto guardando cosa fa il compilatore con il codice sorgente, in termini di IL che crea. È più che possibile che il compilatore JIT farà cose intelligenti in termini di ottimizzazione di un'assegnazione "logica".

In terzo luogo, sto ignorando i generici, principalmente perché in realtà non conosco la risposta, e in parte perché complicherebbe troppo le cose.

Infine, tutto questo è solo con l'attuale implementazione. Le specifiche C # non specificano molto di ciò, ma sono in realtà un dettaglio di implementazione. C'è chi crede che gli sviluppatori di codici gestiti non dovrebbero davvero preoccuparsene. Non sono sicuro di andare così lontano, ma vale la pena immaginare un mondo in cui in realtà tutte le variabili locali vivono sull'heap - che sarebbe comunque conforme alle specifiche.


Esistono due diverse situazioni con l' newoperatore per i tipi di valore: è possibile chiamare un costruttore senza parametri (ad es. new Guid()) O un costruttore con parametri (ad es new Guid(someString).). Questi generano IL significativamente diversi. Per capire perché, è necessario confrontare le specifiche C # e CLI: secondo C #, tutti i tipi di valore hanno un costruttore senza parametri. Secondo le specifiche CLI, nessun tipo di valore ha costruttori senza parametri. (Recupera i costruttori di un tipo di valore con riflessione qualche volta - non ne troverai uno senza parametri.)

Ha senso per C # per trattare il "inizializzare un valore con zeri" come un costruttore, perché mantiene il linguaggio coerente - si può pensare new(...)come sempre chiamare un costruttore. È logico che la CLI la pensi in modo diverso, poiché non esiste un codice reale da chiamare e certamente nessun codice specifico del tipo.

Fa anche la differenza su cosa farai con il valore dopo averlo inizializzato. L'IL usato per

Guid localVariable = new Guid(someString);

è diverso dall'IL utilizzato per:

myInstanceOrStaticVariable = new Guid(someString);

Inoltre, se il valore viene utilizzato come valore intermedio, ad esempio un argomento per una chiamata al metodo, le cose sono di nuovo leggermente diverse. Per mostrare tutte queste differenze, ecco un breve programma di test. Non mostra la differenza tra variabili statiche e variabili di istanza: l'IL differirebbe tra stflde stsfld, ma questo è tutto.

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());
    }
}

Ecco l'IL per la classe, esclusi i bit irrilevanti (come 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
}

Come puoi vedere, ci sono molte diverse istruzioni utilizzate per chiamare il costruttore:

  • newobj: Alloca il valore nello stack, chiama un costruttore con parametri. Utilizzato per valori intermedi, ad esempio per l'assegnazione a un campo o utilizzare come argomento del metodo.
  • call instance: Utilizza una posizione di archiviazione già allocata (in pila o meno). Questo è usato nel codice sopra per l'assegnazione a una variabile locale. Se alla stessa variabile locale viene assegnato un valore più volte utilizzando più newchiamate, inizializza semplicemente i dati sopra il valore precedente , non alloca più spazio di stack ogni volta.
  • initobj: Utilizza una posizione di archiviazione già allocata e cancella solo i dati. Questo è usato per tutte le nostre chiamate di costruttore senza parametri, comprese quelle che assegnano a una variabile locale. Per la chiamata al metodo, viene effettivamente introdotta una variabile locale intermedia e il suo valore viene cancellato da initobj.

Spero che ciò dimostri quanto sia complicato l'argomento, mentre allo stesso tempo fa luce su di esso. In alcuni sensi concettuali, ogni chiamata a newallocare spazio nello stack - ma come abbiamo visto, non è quello che succede davvero anche a livello di IL. Vorrei evidenziare un caso particolare. Prendi questo metodo:

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

Che "logicamente" ha 4 allocazioni di stack - una per la variabile e una per ognuna delle tre newchiamate - ma in effetti (per quel codice specifico) lo stack viene allocato una sola volta, quindi viene riutilizzato lo stesso percorso di archiviazione.

EDIT: Giusto per essere chiari, questo è vero solo in alcuni casi ... in particolare, il valore di guidnon sarà visibile se il Guidcostruttore genera un'eccezione, motivo per cui il compilatore C # è in grado di riutilizzare lo stesso slot dello stack. Vedi il post sul blog di Eric Lippert sulla costruzione del tipo di valore per maggiori dettagli e un caso in cui non si applica.

Ho imparato molto scrivendo questa risposta - chiedi chiarimenti se qualcuno di questi non è chiaro!


1
Jon, il codice di esempio HowManyStackAllocations è buono. Ma potresti cambiarlo per usare un Struct anziché Guid, o aggiungere un nuovo esempio di Struct. Penso che poi affronterebbe direttamente la domanda originale di @ kedar.
Ash,

9
Guid è già una struttura. Vedi msdn.microsoft.com/en-us/library/system.guid.aspx Non avrei scelto un tipo di riferimento per questa domanda :)
Jon Skeet,

1
Cosa succede quando hai List<Guid>e aggiungi quei 3 ad esso? Sarebbe 3 allocazioni (stesso IL)? Ma sono tenuti in qualche posto magico
Arec Barrwin,

1
@Ani: ti manca il fatto che l'esempio di Eric abbia un blocco try / catch - quindi se viene generata un'eccezione durante il costruttore della struttura, devi essere in grado di vedere il valore prima del costruttore. Il mio esempio non ha una situazione del genere: se il costruttore fallisce con un'eccezione, non importa se il valore di guidè stato solo mezzo sovrascritto, poiché non sarà comunque visibile.
Jon Skeet,

2
@Ani: In effetti, Eric lo chiama in fondo al suo post: "Ora, che dire del punto di Wesner? Sì, in effetti se si tratta di una variabile locale allocata in pila (e non di un campo in una chiusura) che viene dichiarata allo stesso livello di "prova" di annidamento della chiamata del costruttore, non passiamo attraverso questo rigamarole di creare un nuovo temporaneo, inizializzare il temporaneo e copiarlo sul locale. In quel caso specifico (e comune) possiamo ottimizzare via la creazione del temporaneo e della copia perché è impossibile per un programma C # osservare la differenza! "
Jon Skeet,

40

La memoria contenente i campi di una struttura può essere allocata sullo stack o sull'heap a seconda delle circostanze. Se la variabile di tipo struct è una variabile locale o un parametro che non viene acquisito da un delegato anonimo o da una classe iteratore, verrà allocato nello stack. Se la variabile fa parte di una classe, verrà allocata all'interno della classe sull'heap.

Se la struttura è allocata sull'heap, non è effettivamente necessario chiamare il nuovo operatore per allocare la memoria. L'unico scopo sarebbe quello di impostare i valori del campo in base a ciò che è nel costruttore. Se il costruttore non viene chiamato, tutti i campi avranno i loro valori predefiniti (0 o null).

Allo stesso modo per le strutture allocate nello stack, tranne per il fatto che C # richiede che tutte le variabili locali siano impostate su un valore prima di essere utilizzate, quindi è necessario chiamare un costruttore personalizzato o il costruttore predefinito (un costruttore che non accetta parametri è sempre disponibile per struct).


13

Per dirla in modo compatto, new è un termine improprio per le strutture, chiamare new semplicemente chiama il costruttore. L'unica posizione di archiviazione per la struttura è la posizione in cui è definita.

Se si tratta di una variabile membro, viene archiviata direttamente in qualunque sia definita, se si tratta di una variabile locale o di un parametro, viene archiviata nello stack.

Contrasta questo con le classi, che hanno un riferimento ovunque la struttura sarebbe stata memorizzata nella sua interezza, mentre il riferimento punta da qualche parte nell'heap. (Membro all'interno, locale / parametro in pila)

Potrebbe essere utile esaminare un po 'il C ++, dove non esiste una vera distinzione tra class / struct. (Esistono nomi simili nella lingua, ma si riferiscono solo all'accessibilità predefinita delle cose) Quando chiami nuovo ottieni un puntatore alla posizione dell'heap, mentre se hai un riferimento non puntatore viene memorizzato direttamente nello stack o all'interno dell'altro oggetto, ala si basa su C #.


5

Come per tutti i tipi di valore, le strutture vanno sempre dove sono state dichiarate .

Vedi questa domanda qui per maggiori dettagli su quando usare le strutture. E questa domanda qui per qualche informazione in più sulle strutture.

Modifica: ho risposto a malincuore che vanno SEMPRE nello stack. Questo è errato .


"Le strutture vanno sempre dove sono state dichiarate", questo è un po 'fuorviante e confuso. Un campo struct in una classe viene sempre inserito nella "memoria dinamica quando viene costruita un'istanza del tipo" - Jeff Richter. Questo può essere indirettamente sull'heap, ma non è affatto uguale a un normale tipo di riferimento.
Ash,

No, penso che sia esattamente giusto, anche se non è lo stesso di un tipo di riferimento. Il valore di una variabile vive dove è dichiarato. Il valore di una variabile del tipo di riferimento è un riferimento, anziché i dati effettivi, tutto qui.
Jon Skeet,

In breve, ogni volta che si crea (dichiara) un tipo di valore in qualsiasi punto di un metodo, questo viene sempre creato nello stack.
Ash,

2
Jon, ti manca il mio punto. Il motivo per cui questa domanda è stata posta per la prima volta è che non è chiaro a molti sviluppatori (me compreso fino a quando non leggo CLR via C #) dove viene allocata una struttura se si utilizza il nuovo operatore per crearla. Dire "le strutture vanno sempre dove sono state dichiarate" non è una risposta chiara.
Ash,

1
@Ash: se avrò tempo, proverò a scrivere una risposta quando arrivo al lavoro. È un argomento troppo grande per cercare di coprire il treno :)
Jon Skeet,

4

Probabilmente mi manca qualcosa qui, ma perché ci preoccupiamo dell'allocazione?

I tipi di valore vengono passati per valore;) e quindi non possono essere mutati in un ambito diverso da quello in cui sono definiti. Per poter mutare il valore devi aggiungere la parola chiave [ref].

I tipi di riferimento vengono passati per riferimento e possono essere mutati.

Esistono ovviamente stringhe di tipi di riferimento immutabili che sono le più popolari.

Layout / inizializzazione array: tipi di valore -> memoria zero [nome, zip] [nome, zip] Tipi di riferimento -> memoria zero -> null [ref] [ref]


3
I tipi di riferimento non vengono passati per riferimento - i riferimenti vengono passati per valore. È molto diverso.
Jon Skeet,

2

Una dichiarazione classo structè come un modello utilizzato per creare istanze o oggetti in fase di esecuzione. Se si definisce una classo structchiamata Persona, Persona è il nome del tipo. Se si dichiara e si inizializza una variabile p di tipo Person, si dice che p è un oggetto o un'istanza di Person. È possibile creare più istanze dello stesso tipo Persona e ciascuna istanza può avere valori diversi nelle sue propertiese fields.

A classè un tipo di riferimento. Quando un oggetto diclass viene creato , la variabile a cui è assegnato l'oggetto contiene solo un riferimento a quella memoria. Quando il riferimento all'oggetto viene assegnato a una nuova variabile, la nuova variabile si riferisce all'oggetto originale. Le modifiche apportate attraverso una variabile si riflettono nell'altra variabile perché si riferiscono entrambi agli stessi dati.

A structè un tipo di valore. Quando structviene creato un, la variabile a cui structè assegnato contiene i dati effettivi della struttura. Quando structviene assegnato a una nuova variabile, viene copiato. La nuova variabile e la variabile originale contengono quindi due copie separate degli stessi dati. Le modifiche apportate a una copia non influiscono sull'altra copia.

In generale, classesvengono utilizzati per modellare comportamenti più complessi o dati che si intende modificare dopo la classcreazione di un oggetto. Structssono più adatti per piccole strutture di dati che contengono principalmente dati che non sono destinati a essere modificati dopo la structcreazione.

per più...


1

Praticamente le strutture considerate tipi di valore sono allocate nello stack, mentre gli oggetti vengono allocati nell'heap, mentre il riferimento all'oggetto (puntatore) viene allocato nello stack.


1

Le strutture vengono assegnate allo stack. Ecco una spiegazione utile:

Structs

Inoltre, le classi quando vengono istanziate all'interno di .NET allocano la memoria nell'heap o nello spazio di memoria riservato di .NET. Considerando che le strutture producono più efficienza se istanziate a causa dell'allocazione in pila. Inoltre, va notato che il passaggio dei parametri all'interno delle strutture viene effettuato in base al valore.


5
Questo non copre il caso in cui una struct fa parte di una classe - a quel punto vive nell'heap, con il resto dei dati dell'oggetto.
Jon Skeet,

1
Sì, ma in realtà si concentra e risponde alla domanda che viene posta. Votato.
Ash,

... pur essendo errato e fuorviante. Siamo spiacenti, ma non ci sono risposte brevi a questa domanda: Jeffrey è l'unica risposta completa.
Marc Gravell
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.