Quando si crea un'istanza di una classe con l' new
operatore, la memoria viene allocata sull'heap. Quando si crea un'istanza di una struttura con l' new
operatore da dove viene allocata la memoria, sull'heap o sullo stack?
Quando si crea un'istanza di una classe con l' new
operatore, la memoria viene allocata sull'heap. Quando si crea un'istanza di una struttura con l' new
operatore da dove viene allocata la memoria, sull'heap o sullo stack?
Risposte:
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 new
dell'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' new
operatore 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 stfld
e 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ù new
chiamate, 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 new
allocare 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 new
chiamate - 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 guid
non sarà visibile se il Guid
costruttore 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!
List<Guid>
e aggiungi quei 3 ad esso? Sarebbe 3 allocazioni (stesso IL)? Ma sono tenuti in qualche posto magico
guid
è stato solo mezzo sovrascritto, poiché non sarà comunque visibile.
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).
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 #.
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 .
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]
Una dichiarazione class
o struct
è come un modello utilizzato per creare istanze o oggetti in fase di esecuzione. Se si definisce una class
o struct
chiamata 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 properties
e 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 struct
viene creato un, la variabile a cui struct
è assegnato contiene i dati effettivi della struttura. Quando struct
viene 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, classes
vengono utilizzati per modellare comportamenti più complessi o dati che si intende modificare dopo la class
creazione di un oggetto. Structs
sono più adatti per piccole strutture di dati che contengono principalmente dati che non sono destinati a essere modificati dopo la struct
creazione.
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.
Le strutture vengono assegnate allo stack. Ecco una spiegazione utile:
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.