Perché le strutture vengono archiviate nello stack mentre le classi vengono archiviate nell’heap (.NET)?

So che una delle differenze tra classi e strutture è che le istanze di struct vengono archiviate nello stack e le istanze di class (oggetti) sono memorizzate nell’heap.

Poiché le classi e le strutture sono molto simili. Qualcuno conosce la differenza per questa particolare distinzione?

(modificato per coprire punti nei commenti)

Per sottolineare: ci sono differenze e somiglianze tra tipi di valori e tipi di riferimento, ma queste differenze non hanno nulla a che fare con lo stack vs heap e tutto ciò che ha a che fare con la semantica della copia e la semantica di riferimento. In particolare, se lo facciamo:

Foo first = new Foo { Bar = 123 }; Foo second = first; 

Quindi “primo” e “secondo” parlano della stessa copia di Foo ? o diverse copie? Accade semplicemente che lo stack sia un modo comodo ed efficiente di gestire i tipi di valore come variabili. Ma questo è un dettaglio di implementazione.

(modifica fine)

Re l’intera cosa del tipo “valore va in pila” … – i tipi di valore non sempre vanno in pila;

  • se sono campi in una class
  • se sono in scatola
  • se sono “variabili catturate”
  • se sono in un blocco iteratore

poi vanno sul mucchio (gli ultimi due sono in realtà solo esempi esotici del primo)

vale a dire

 class Foo { int i; // on the heap } static void Foo() { int i = 0; // on the heap due to capture // ... Action act = delegate {Console.WriteLine(i);}; } static IEnumerable Foo() { int i = 0; // on the heap to do iterator block // yield return i; } 

Inoltre, Eric Lippert (come già notato) ha un eccellente blog su questo argomento

È utile in pratica essere in grado di allocare memoria nello stack per alcuni scopi, dal momento che tali allocazioni sono molto veloci.

Tuttavia, vale la pena notare che non vi è alcuna garanzia fondamentale che tutte le strutture vengano messe in pila. Eric Lippert ha recentemente scritto un interessante post di blog su questo argomento.

Questa è una grande domanda; Non l’ho coperto nell’articolo a cui è collegato Marc Gravell. Ecco la seconda parte:

http://blogs.msdn.com/ericlippert/archive/2009/05/04/the-stack-is-an-implementation-detail-part-two.aspx

Ogni processo ha un blocco di dati costituito da due segmenti di memoria allocabili diversi. Questi sono stack e heap. Lo stack serve principalmente come gestore di stream del programma e salva variabili locali, parametri e puntatori di ritorno (in caso di ritorno dalla funzione di lavoro corrente).

Le classi sono molto complesse e per lo più di grandi dimensioni rispetto ai tipi di valore come le strutture (o tipi di base – int, char, ecc.) Poiché l’allocazione dello stack dovrebbe essere specializzata sull’efficienza del stream del programma, non serve un ambiente ottimale da mantenere oggetti di grandi dimensioni.

Pertanto, per salutare entrambe le aspettative, questa architettura separata è arrivata.

Il modo in cui il compilatore e l’ambiente run-time gestiscono la gestione della memoria è cresciuto per un lungo periodo di tempo. La decisione di allocare la memoria dello stack rispetto all’heap della memoria aveva molto a che fare con ciò che poteva essere conosciuto in fase di compilazione e ciò che poteva essere conosciuto in fase di runtime. Questo era prima dei tempi di esecuzione gestiti.

In generale, il compilatore ha un ottimo controllo di ciò che è in pila, arriva a decidere cosa viene ripulito e in base alle convenzioni di chiamata. Il mucchio d’altra parte era più simile al selvaggio west. Il compilatore non aveva un buon controllo su quando le cose andavano e venivano. Posizionando gli argomenti di funzione nello stack, il compilatore è in grado di creare un ambito , che può essere controllato per tutta la durata della chiamata. Questo è un posto naturale per mettere i tipi di valore, perché sono facili da controllare rispetto ai tipi di riferimento che possono distribuire le locazioni di memoria (puntatori) a chiunque desideri.

La moderna gestione della memoria cambia molto. Il runtime .NET può assumere il controllo dei tipi di riferimento e dell’heap gestito tramite complessi algoritmi di garbage collection e gestione della memoria. Questo è anche un argomento molto, molto profondo .

Ti consiglio di leggere alcuni testi sui compilatori – sono cresciuto su Aho, quindi lo consiglio . Puoi anche imparare molto sull’argomento leggendo Gosling .

In alcune lingue, come C ++, gli oggetti sono anche tipi di valore.

Trovare un esempio per l’opposto è più difficile, ma con il classico Pascal le strutture di unione possono essere istanziate solo nell’heap. (le normali strutture potrebbero essere statiche)

In breve: questa situazione è una scelta, non una dura legge. Poiché C # (e Java prima di esso) mancano di basi procedurali, ci si può chiedere perché abbia bisogno di strutture.

La ragione per cui esiste, è probabilmente una combinazione di averne bisogno per interfacce esterne e di avere un tipo performante e complesso (contenitore) stretto. Uno che è più veloce della class. E allora è meglio renderlo un tipo di valore.

Marc Gravell ha già spiegato meravigliosamente la differenza su come vengono copiati i tipi di valore e di riferimento, che è la principale differenza tra loro.

Per quanto riguarda il motivo per cui i tipi di valore vengono solitamente creati nello stack, è perché il modo in cui vengono copiati lo consente. Lo stack presenta alcuni vantaggi definiti rispetto all’heap in termini di prestazioni, in particolare perché il compilatore può calcolare la posizione esatta di una variabile creata in un determinato blocco di codice, che rende più rapido l’accesso.

Quando crei un tipo di riferimento, ricevi un riferimento all’object reale che esiste nell’heap. C’è un piccolo livello di riferimento indiretto ogni volta che interagisci con l’object stesso. Questi tipi di riferimento non possono essere creati nello stack perché la durata dei valori nello stack è determinata, in gran parte, dalla struttura del codice. La funzione frame di una chiamata al metodo verrà estratta dallo stack quando la funzione restituisce, ad esempio.

Con i tipi di valore, tuttavia, la loro semantica della copia consente al compilatore, a seconda di dove è stato creato, di inserirlo nello stack. Se crei una variabile locale che contiene un’istanza di una struct in un metodo e poi la restituisce, verrà creata una sua copia, come spiegato in precedenza da Marc. Ciò significa che il valore può essere collocato in modo sicuro nello stack, poiché la durata dell’istanza effettiva è legata al frame della funzione del metodo. Ogni volta che lo invii da qualche parte al di fuori della funzione corrente, verrà creata una copia di esso, quindi non importa se si lega l’esistenza dell’istanza originale all’ambito della funzione. Seguendo queste linee, puoi anche capire perché i tipi di valore catturati dalle chiusure devono andare nell’heap: sopravvivono al loro scopo perché devono essere accessibili anche dalla chiusura, che può essere passata liberamente.

Se si trattasse di un tipo di riferimento, non si restituirebbe una copia dell’object, ma piuttosto un riferimento, il che significa che il valore effettivo deve essere memorizzato altrove, altrimenti, se si restituisce il riferimento e la durata dell’object è legata a lo scopo in cui è stato creato, finirebbe per indicare uno spazio vuoto nella memoria.

La distinzione in realtà non è che “I tipi di valore vanno in pila, i tipi di riferimento sull’heap”. Il punto reale è che di solito è più efficiente accedere agli oggetti che vivono nello stack, così il compilatore proverà e inserirà quei valori che può lì. Risulta semplicemente che i tipi di valore, a causa della loro semantica della copia, si adattano meglio alla fattura rispetto ai tipi di riferimento.

Credo che usare o meno lo stack o lo spazio heap sia la distinzione principale tra i due, forse questo articolo farà luce sulla tua domanda: Classi di csharp e struct

La differenza principale consiste nel fatto che l’heap può contenere oggetti che vivono per sempre mentre qualcosa nello stack è temporaneo in quanto scomparirà quando il callsite recintato viene chiuso. Questo perché quando si entra in un metodo cresce per contenere le variabili locali e il metodo del chiamante. Quando il metodo esce (ab) normalmente, ad es. Return o a causa di un’eccezione, ogni frame deve essere estratto dallo stack. Alla fine la cornice interessata è spuntata e tutto ciò che è andato perso.

L’intero punto sull’utilizzo dello stack è che implementa e onora automaticamente l’ambito. Una variabile memorizzata nello stack esiste fino a quando non viene chiusa la funzione che lo ha creato e vengono generate le funzioni stack frame. Le cose che hanno uno scope locale sono naturali per gli oggetti di storage stack che hanno scope più grandi sono più difficili da gestire in pila. Gli oggetti nell’heap possono avere vite che vengono controllate in modi più complessi.

I compilatori utilizzano sempre lo stack per le variabili: il valore o il riferimento fa poca differenza. Una variabile di riferimento non deve avere il suo valore memorizzato nello stack – può essere ovunque e l’heap rende più efficiente se l’object di riferimento è grande e se ci sono più riferimenti ad esso. Il punto è che l’ambito di una variabile di riferimento non è lo stesso della vita dell’object a cui fa riferimento, cioè una variabile può essere distrutta venendo scartata dallo stack ma l’object (sull’heap) su cui i riferimenti potrebbero vivere.

Se un tipo di valore è abbastanza piccolo, si potrebbe anche archiviarlo nello stack al posto di un riferimento ad esso sull’heap – la sua durata è legata all’ambito della variabile. Se il tipo di valore fa parte di un tipo di riferimento più grande, anch’esso potrebbe avere più riferimenti e quindi è più naturale archiviarlo nell’heap e dissociarne la durata da una singola variabile di riferimento.

Lo stack e l’heap durano circa una vita e il valore v semantica di riferimento è quasi un prodotto per prodotto.

Dai un’occhiata a Valore e riferimento

I tipi di valore vanno in pila, i tipi di riferimento vanno sullo heap. Una struct è un tipo di valore.

Tuttavia, non ci sono garanzie riguardo a questo nella specifica, quindi potrebbe cambiare nelle versioni future 🙂