Weist die Verwendung von "new" für eine Struktur diese dem Heap oder Stack zu?

290

Wenn Sie mit dem newOperator eine Instanz einer Klasse erstellen , wird Speicher auf dem Heap zugewiesen. Wenn Sie mit dem newOperator eine Instanz einer Struktur erstellen, wo wird der Speicher zugewiesen, auf dem Heap oder auf dem Stapel?

kedar kamthe
quelle

Antworten:

305

Okay, mal sehen, ob ich das klarer machen kann.

Zum einen ist Ash Recht: Die Frage ist nicht über dem Werttyp Variablen zugewiesen werden. Das ist eine andere Frage - und eine, auf die die Antwort nicht nur "auf dem Stapel" ist. Es ist komplizierter als das (und wird durch C # 2 noch komplizierter). Ich habe einen Artikel zu diesem Thema und werde ihn auf Anfrage erweitern, aber wir wollen uns nur mit dem newOperator befassen .

Zweitens hängt das alles wirklich davon ab, über welches Level Sie sprechen. Ich schaue mir an, was der Compiler mit dem Quellcode macht, in Bezug auf die IL, die er erstellt. Es ist mehr als möglich, dass der JIT-Compiler clevere Dinge unternimmt, um eine Menge "logischer" Zuordnungen zu optimieren.

Drittens ignoriere ich Generika, hauptsächlich weil ich die Antwort nicht kenne und teilweise weil es die Dinge zu sehr komplizieren würde.

Schließlich ist dies alles nur mit der aktuellen Implementierung. Die C # -Spezifikation spezifiziert nicht viel davon - es ist effektiv ein Implementierungsdetail. Es gibt Leute, die glauben, dass Entwickler von verwaltetem Code sich wirklich nicht darum kümmern sollten. Ich bin mir nicht sicher, ob ich so weit gehen würde, aber es lohnt sich, sich eine Welt vorzustellen, in der tatsächlich alle lokalen Variablen auf dem Haufen leben - was immer noch der Spezifikation entspricht.


Es gibt zwei verschiedene Situationen mit dem newOperator für Werttypen: Sie können entweder einen parameterlosen Konstruktor (z. B. new Guid()) oder einen parametrischen Konstruktor (z new Guid(someString). B. ) aufrufen . Diese erzeugen signifikant unterschiedliche IL. Um zu verstehen, warum, müssen Sie die C # - und CLI-Spezifikationen vergleichen: Gemäß C # haben alle Werttypen einen parameterlosen Konstruktor. Gemäß der CLI-Spezifikation haben keine Werttypen parameterlose Konstruktoren. (Rufen Sie einige Zeit die Konstruktoren eines Wertetyps mit Reflexion ab - Sie werden keinen parameterlosen finden.)

Es macht Sinn für C # behandeln die als Konstruktor „einen Wert mit Nullen initialisiert werden “, weil es die Sprache konsistent hält - Sie denken können new(...)wie immer einen Konstruktor aufrufen. Für die CLI ist es sinnvoll, dies anders zu sehen, da kein wirklicher Code zum Aufrufen vorhanden ist - und sicherlich kein typspezifischer Code.

Es macht auch einen Unterschied, was Sie mit dem Wert tun werden, nachdem Sie ihn initialisiert haben. Die IL verwendet für

Guid localVariable = new Guid(someString);

unterscheidet sich von der IL, die verwendet wird für:

myInstanceOrStaticVariable = new Guid(someString);

Wenn der Wert als Zwischenwert verwendet wird, z. B. als Argument für einen Methodenaufruf, sind die Dinge wieder etwas anders. Um all diese Unterschiede aufzuzeigen, finden Sie hier ein kurzes Testprogramm. Es zeigt nicht den Unterschied zwischen statischen Variablen und Instanzvariablen: Die IL würde sich zwischen stfldund unterscheiden stsfld, aber das ist alles.

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

Hier ist die IL für die Klasse, ausgenommen irrelevante Bits (wie z. B. 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
}

Wie Sie sehen können, werden zum Aufrufen des Konstruktors viele verschiedene Anweisungen verwendet:

  • newobj: Ordnet den Wert auf dem Stapel zu und ruft einen parametrisierten Konstruktor auf. Wird für Zwischenwerte verwendet, z. B. für die Zuordnung zu einem Feld oder als Methodenargument.
  • call instance: Verwendet einen bereits zugewiesenen Speicherort (ob auf dem Stapel oder nicht). Dies wird im obigen Code zum Zuweisen zu einer lokalen Variablen verwendet. Wenn derselben lokalen Variablen bei mehreren newAufrufen mehrmals ein Wert zugewiesen wird , werden die Daten nur über dem alten Wert initialisiert - es wird nicht jedes Mal mehr Stapelspeicher zugewiesen.
  • initobj: Verwendet einen bereits zugewiesenen Speicherort und löscht nur die Daten. Dies wird für alle unsere parameterlosen Konstruktoraufrufe verwendet, einschließlich derer, die einer lokalen Variablen zugewiesen werden. Für den Methodenaufruf wird effektiv eine lokale Zwischenvariable eingeführt und ihr Wert gelöscht initobj.

Ich hoffe, dies zeigt, wie kompliziert das Thema ist, während es gleichzeitig ein wenig beleuchtet wird. In gewisser Hinsicht bedeutet jeder Aufruf, newSpeicherplatz auf dem Stapel zuzuweisen - aber wie wir gesehen haben, ist dies selbst auf IL-Ebene nicht der Fall. Ich möchte einen bestimmten Fall hervorheben. Nehmen Sie diese Methode:

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

Das "logisch" hat 4 Stapelzuordnungen - eine für die Variable und eine für jeden der drei newAufrufe - aber tatsächlich (für diesen spezifischen Code) wird der Stapel nur einmal zugewiesen, und dann wird derselbe Speicherort wiederverwendet.

EDIT: Nur um klar zu sein, dies ist nur in einigen Fällen wahr ... insbesondere wird der Wert von guidnicht sichtbar, wenn der GuidKonstruktor eine Ausnahme auslöst, weshalb der C # -Compiler denselben Stapelsteckplatz wiederverwenden kann. Weitere Informationen und einen Fall, in dem dies nicht zutrifft, finden Sie in Eric Lipperts Blogbeitrag zur Werttypkonstruktion.

Ich habe beim Schreiben dieser Antwort viel gelernt - bitte um Klärung, wenn etwas unklar ist!

Jon Skeet
quelle
1
Jon, der HowManyStackAllocations-Beispielcode ist gut. Sie können es jedoch entweder ändern, um eine Struktur anstelle von Guid zu verwenden, oder ein neues Strukturbeispiel hinzufügen. Ich denke, das würde dann direkt die ursprüngliche Frage von @ kedar ansprechen.
Ash
9
Guid ist bereits eine Struktur. Siehe msdn.microsoft.com/en-us/library/system.guid.aspx Ich hätte keinen Referenztyp für diese Frage ausgewählt :)
Jon Skeet
1
Was passiert, wenn Sie List<Guid>diese 3 haben und hinzufügen? Das wären 3 Zuweisungen (gleiche IL)? Aber sie werden an einem magischen Ort aufbewahrt
Arec Barrwin
1
@Ani: Sie vermissen die Tatsache, dass Erics Beispiel einen Try / Catch-Block hat. Wenn also während des Konstruktors der Struktur eine Ausnahme ausgelöst wird, müssen Sie den Wert vor dem Konstruktor sehen können. In meinem Beispiel gibt es keine solche Situation. Wenn der Konstruktor mit einer Ausnahme ausfällt, spielt es keine Rolle, ob der Wert von guidnur zur Hälfte überschrieben wurde, da er ohnehin nicht sichtbar ist.
Jon Skeet
2
@Ani: Tatsächlich ruft Eric dies am Ende seines Beitrags aus: "Was ist nun mit Wesners Punkt? Ja, tatsächlich, wenn es sich um eine vom Stapel zugewiesene lokale Variable handelt (und nicht um ein Feld in einem Abschluss), das deklariert ist Auf der gleichen Ebene der "Versuch" -Verschachtelung wie der Konstruktoraufruf gehen wir nicht durch diese Rigamarole, ein neues temporäres zu erstellen, das temporäre zu initialisieren und es in das lokale zu kopieren. In diesem speziellen (und allgemeinen) Fall können wir optimieren die Erstellung der temporären und der Kopie, weil es für ein C # -Programm unmöglich ist, den Unterschied zu beobachten! "
Jon Skeet
40

Der Speicher, der die Felder einer Struktur enthält, kann je nach den Umständen entweder auf dem Stapel oder auf dem Heap zugewiesen werden. Wenn die Variable vom Typ Struktur eine lokale Variable oder ein lokaler Parameter ist, der nicht von einem anonymen Delegaten oder einer Iteratorklasse erfasst wird, wird sie dem Stapel zugewiesen. Wenn die Variable Teil einer Klasse ist, wird sie innerhalb der Klasse auf dem Heap zugewiesen.

Wenn die Struktur auf dem Heap zugewiesen ist, ist das Aufrufen des neuen Operators nicht erforderlich, um den Speicher zuzuweisen. Der einzige Zweck wäre, die Feldwerte entsprechend den Angaben im Konstruktor festzulegen. Wenn der Konstruktor nicht aufgerufen wird, erhalten alle Felder ihre Standardwerte (0 oder null).

Ähnliches gilt für auf dem Stapel zugewiesene Strukturen, mit der Ausnahme, dass für C # alle lokalen Variablen auf einen bestimmten Wert festgelegt werden müssen, bevor sie verwendet werden. Daher müssen Sie entweder einen benutzerdefinierten Konstruktor oder den Standardkonstruktor aufrufen (ein Konstruktor, der keine Parameter akzeptiert, ist immer verfügbar Strukturen).

Jeffrey L Whitledge
quelle
13

Um es kompakt auszudrücken: new ist eine Fehlbezeichnung für Strukturen. Der Aufruf von new ruft einfach den Konstruktor auf. Der einzige Speicherort für die Struktur ist der Speicherort, den sie definiert hat.

Wenn es sich um eine Mitgliedsvariable handelt, wird sie direkt in dem gespeichert, in dem sie definiert ist. Wenn es sich um eine lokale Variable oder einen lokalen Parameter handelt, wird sie auf dem Stapel gespeichert.

Vergleichen Sie dies mit Klassen, die überall dort eine Referenz haben, wo die Struktur vollständig gespeichert worden wäre, während die Referenz irgendwo auf dem Heap verweist. (Mitglied innerhalb, lokal / Parameter auf Stapel)

Es kann hilfreich sein, ein wenig in C ++ zu schauen, wo es keinen wirklichen Unterschied zwischen Klasse / Struktur gibt. (Es gibt ähnliche Namen in der Sprache, aber sie beziehen sich nur auf die Standardzugänglichkeit von Dingen.) Wenn Sie new aufrufen, erhalten Sie einen Zeiger auf den Heap-Speicherort. Wenn Sie eine Nicht-Zeiger-Referenz haben, wird diese direkt auf dem Stapel oder gespeichert innerhalb des anderen Objekts strukturiert ala in C #.

Guvante
quelle
5

Wie bei allen Werttypen gehen Strukturen immer dorthin, wo sie deklariert wurden .

In dieser Frage finden Sie weitere Informationen zur Verwendung von Strukturen. Und diese Frage hier für weitere Informationen zu Strukturen.

Edit: Ich hatte fälschlicherweise geantwortet, dass sie IMMER in den Stapel gehen. Das ist falsch .

Esteban Araya
quelle
"Strukturen gehen immer dorthin, wo sie deklariert wurden", das ist etwas irreführend verwirrend. Ein Strukturfeld in einer Klasse wird immer in den "dynamischen Speicher gelegt, wenn eine Instanz des Typs erstellt wird" - Jeff Richter. Dies kann indirekt auf dem Heap sein, ist jedoch überhaupt nicht mit einem normalen Referenztyp identisch.
Ash
Nein, ich denke, es ist genau richtig - auch wenn es nicht mit einem Referenztyp identisch ist. Der Wert einer Variablen lebt dort, wo sie deklariert ist. Der Wert einer Referenztypvariablen ist eine Referenz, anstelle der tatsächlichen Daten ist das alles.
Jon Skeet
Zusammenfassend lässt sich sagen, dass ein Werttyp immer dann, wenn Sie ihn irgendwo in einer Methode erstellen (deklarieren), auf dem Stapel erstellt wird.
Ash
2
Jon, du vermisst meinen Standpunkt. Der Grund, warum diese Frage zum ersten Mal gestellt wurde, ist, dass vielen Entwicklern (ich eingeschlossen, bis ich CLR über C # gelesen habe) nicht klar ist, wo eine Struktur zugewiesen wird, wenn Sie den neuen Operator zum Erstellen verwenden. Die Aussage "Strukturen gehen immer dorthin, wo sie deklariert wurden" ist keine klare Antwort.
Ash
1
@Ash: Wenn ich Zeit habe, werde ich versuchen, eine Antwort zu schreiben, wenn ich zur Arbeit komme. Es ist ein zu großes Thema, um es im Zug zu behandeln :)
Jon Skeet
4

Ich vermisse hier wahrscheinlich etwas, aber warum interessiert uns die Zuteilung?

Werttypen werden als Wert übergeben;) und können daher nicht in einem anderen Bereich als dem, in dem sie definiert sind, mutiert werden. Um den Wert ändern zu können, müssen Sie das Schlüsselwort [ref] hinzufügen.

Referenztypen werden als Referenz übergeben und können mutiert werden.

Es gibt natürlich unveränderliche Referenztypen, die am beliebtesten sind.

Array-Layout / Initialisierung: Werttypen -> Nullspeicher [Name, Zip] [Name, Zip] Referenztypen -> Nullspeicher -> Null [Ref] [Ref]

user18579
quelle
3
Referenztypen werden nicht als Referenz übergeben - Referenzen werden als Wert übergeben. Das ist ganz anders.
Jon Skeet
2

Eine classoder struct-Deklaration ist wie eine Blaupause, mit der Instanzen oder Objekte zur Laufzeit erstellt werden. Wenn Sie eine classoder eine structangerufene Person definieren, ist Person der Name des Typs. Wenn Sie eine Variable p vom Typ Person deklarieren und initialisieren, wird p als Objekt oder Instanz von Person bezeichnet. Es können mehrere Instanzen desselben Personentyps erstellt werden, und jede Instanz kann unterschiedliche Werte in ihrem propertiesund haben fields.

A classist ein Referenztyp. Wenn ein Objekt von classerstellt wird, enthält die Variable, der das Objekt zugewiesen ist, nur einen Verweis auf diesen Speicher. Wenn die Objektreferenz einer neuen Variablen zugewiesen wird, bezieht sich die neue Variable auf das ursprüngliche Objekt. Über eine Variable vorgenommene Änderungen werden in der anderen Variablen wiedergegeben, da beide auf dieselben Daten verweisen.

A structist ein Werttyp. Wenn a structerstellt wird, enthält die Variable, der das structzugewiesen ist, die tatsächlichen Daten der Struktur. Wenn das structeiner neuen Variablen zugewiesen ist, wird es kopiert. Die neue Variable und die ursprüngliche Variable enthalten daher zwei separate Kopien derselben Daten. Änderungen an einer Kopie wirken sich nicht auf die andere Kopie aus.

Im Allgemeinen classeswerden sie verwendet, um komplexeres Verhalten oder Daten zu modellieren, die nach dem Erstellen eines classObjekts geändert werden sollen . Structssind am besten für kleine Datenstrukturen geeignet, die hauptsächlich Daten enthalten, die nach der Erstellung nicht geändert werden sollen struct.

für mehr...

Sujit
quelle
1

Ziemlich genau die Strukturen, die als Werttypen betrachtet werden, werden auf dem Stapel zugewiesen, während Objekte auf dem Heap zugewiesen werden, während die Objektreferenz (Zeiger) auf dem Stapel zugewiesen wird.

bashmohandes
quelle
1

Strukturen werden dem Stapel zugeordnet. Hier ist eine hilfreiche Erklärung:

Strukturen

Darüber hinaus weisen Klassen, wenn sie in .NET instanziiert werden, Speicher auf dem Heap oder dem reservierten Speicherplatz von .NET zu. Während Strukturen aufgrund der Zuweisung auf dem Stapel eine höhere Effizienz erzielen, wenn sie instanziiert werden. Darüber hinaus sollte beachtet werden, dass die Übergabe von Parametern innerhalb von Strukturen nach Wert erfolgt.

DaveK
quelle
5
Dies gilt nicht für den Fall, dass eine Struktur Teil einer Klasse ist - zu diesem Zeitpunkt befindet sie sich auf dem Heap mit den restlichen Daten des Objekts.
Jon Skeet
1
Ja, aber es konzentriert sich tatsächlich auf die gestellte Frage und beantwortet sie. Abgestimmt.
Ash
... während immer noch falsch und irreführend. Entschuldigung, aber es gibt keine kurzen Antworten auf diese Frage - Jeffrey's ist die einzige vollständige Antwort.
Marc Gravell