webentwicklung-frage-antwort-db.com.de

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

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

277
kedar kamthe

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

Erstens hat Ash recht: Die Frage ist nicht , wo Werttyp Variablen zugeordnet sind. Das ist eine andere Frage - und eine, auf die die Antwort nicht nur "auf dem Stapel" lautet. Es ist komplizierter (und durch C # 2 noch komplizierter). Ich habe ein Artikel zum Thema und werde es auf Anfrage erweitern, aber lasst uns nur mit dem new -Operator umgehen.

Zweitens hängt das alles wirklich davon ab, über welches Niveau Sie sprechen. Ich schaue, 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 tut, um eine Menge "logischer" Zuweisungen zu vermeiden.

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 es den Entwicklern von verwaltetem Code eigentlich egal sein sollte. 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 entsprechen würde.


Es gibt zwei verschiedene Situationen mit dem Operator new für Werttypen: Sie können entweder einen parameterlosen Konstruktor (z. B. new Guid()) oder einen parameterreichen Konstruktor (z. B. new Guid(someString)) 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. (Holen Sie sich die Konstruktoren eines Werttyps mit Reflektion einige Zeit - Sie werden keine parameterlose finden.)

Es ist sinnvoll, dass C # den "Initialisiere einen Wert mit Nullen" als Konstruktor behandelt, da er die Sprache konsistent hält - Sie können sich new(...) als immer Aufruf eines Konstruktors. Es ist sinnvoll, dass die CLI das anders betrachtet, da es keinen echten Code zum Aufrufen gibt - und schon gar keinen typspezifischen Code.

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

Guid localVariable = new Guid(someString);

unterscheidet sich von der IL für:

myInstanceOrStaticVariable = new Guid(someString);

Wenn der Wert zusätzlich als ein Zwischenwert verwendet wird, z. ein Argument für einen Methodenaufruf, die Dinge sind wieder etwas anders. Um all diese Unterschiede aufzuzeigen, folgt ein kurzes Testprogramm. Es zeigt nicht den Unterschied zwischen statischen Variablen und Instanzvariablen: Die IL würde sich zwischen stfld und stsfld unterscheiden, 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, ohne irrelevante Bits (wie 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, gibt es viele verschiedene Anweisungen zum Aufrufen des Konstruktors:

  • newobj: Weist den Wert auf dem Stack zu und ruft einen parametrisierten Konstruktor auf. Wird für Zwischenwerte verwendet, z. für die Zuweisung zu einem Feld oder als Methodenargument verwenden.
  • call instance: Verwendet einen bereits zugewiesenen Speicherort (ob auf dem Stapel oder nicht). Dies wird im obigen Code für die Zuweisung zu einer lokalen Variablen verwendet. Wenn der gleichen lokalen Variablen mit mehreren new -Aufrufen mehrmals ein Wert zugewiesen wird, werden die Daten nur über dem alten Wert initialisiert - dies ist nicht der Fall Jedes Mal mehr Stapelspeicher zuweisen.
  • 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 sind. Für den Methodenaufruf wird effektiv eine lokale Zwischenvariable eingeführt, deren Wert von initobj gelöscht wird.

Ich hoffe, dies zeigt, wie kompliziert das Thema ist, während es gleichzeitig beleuchtet wird. In einigen begrifflichen Sinnen weist jeder Aufruf von new Speicherplatz auf dem Stapel zu - aber wie wir gesehen haben, ist dies nicht der Fall passiert wirklich auch auf der IL-Ebene. 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 Stapelzuweisungen - eine für die Variable und eine für jeden der drei new Aufrufe - aber tatsächlich (für diesen spezifischen Code) wird der Stapel nur einmal zugewiesen und dann derselbe Speicher Speicherort wird wiederverwendet.

BEARBEITEN: Nur um klar zu sein, dies gilt nur in einigen Fällen ... Insbesondere wird der Wert von guid nicht angezeigt, wenn der Konstruktor Guid eine Ausnahme auslöst, weshalb Der C # -Compiler kann denselben Stack-Slot wiederverwenden. Weitere Informationen finden Sie in Eric Lipperts Blogbeitrag zur Werttypkonstruktion und in einem Fall, in dem es nicht gilt .

Ich habe beim Schreiben dieser Antwort viel gelernt - bitte fragen Sie nach, wenn etwas unklar ist!

298
Jon Skeet

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 struct eine lokale Variable oder ein lokaler Parameter ist, die bzw. der nicht von einer anonymen Delegat- oder Iteratorklasse erfasst wird, wird sie auf dem Stapel zugeordnet. Wenn die Variable Teil einer Klasse ist, wird sie innerhalb der Klasse auf dem Heap zugeordnet.

Wenn die Struktur auf dem Heap zugeordnet ist, muss der Operator new nicht aufgerufen werden, um den Speicher zuzuordnen. Der einzige Zweck wäre, die Feldwerte entsprechend dem Inhalt des Konstruktors 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 in C # alle lokalen Variablen auf einen bestimmten Wert gesetzt 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 für verfügbar) structs).

38

Um es kurz zu machen, new ist eine falsche Bezeichnung für structs, new ruft einfach den Konstruktor auf. Der einzige Speicherort für die Struktur ist der definierte Speicherort.

Wenn es sich um eine Mitgliedsvariable handelt, wird sie direkt in dem Bereich 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 einen Verweis haben, wo die Struktur vollständig gespeichert worden wäre, während der Verweis irgendwo auf dem Heap liegt. (Member innerhalb, local/parameter on stack)

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 Standardzugriffsmöglichkeiten von Dingen.) Wenn Sie new aufrufen, erhalten Sie einen Zeiger auf den Heap-Speicherort. Wenn Sie einen Nicht-Zeiger-Verweis haben, wird dieser direkt auf dem Stack oder gespeichert innerhalb des anderen Objekts, ala Strukturen in C #.

12
Guvante

Wie bei allen Werttypen gehen Strukturen immer dahin, wo sie waren deklariert.

Siehe diese Frage hier für 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 . Dies ist falsch .

5
Esteban Araya

Ich vermisse hier wahrscheinlich etwas, aber warum ist uns die Zuordnung wichtig?

Werttypen werden als Wert übergeben;) und können daher nicht in einem anderen Bereich als dem, in dem sie definiert sind, geändert 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: Wertetypen -> Nullspeicher [Name, Zip] [Name, Zip] Referenztypen -> Nullspeicher -> Null [Ref] [Ref]

4
user18579

Eine class- oder struct -Deklaration ist wie eine Blaupause, mit der Instanzen oder Objekte zur Laufzeit erstellt werden. Wenn Sie eine class oder struct mit dem Namen 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 in ihren properties und fields unterschiedliche Werte aufweisen.

Ein class ist ein Referenztyp. Wenn ein Objekt von class erstellt 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. Änderungen, die über eine Variable vorgenommen wurden, werden in der anderen Variablen wiedergegeben, da beide auf dieselben Daten verweisen.

Ein struct ist ein Werttyp. Wenn ein struct erstellt wird, enthält die Variable, der das struct zugewiesen ist, die tatsächlichen Daten der Struktur. Wenn die struct einer neuen Variablen zugewiesen wird, wird sie kopiert. Die neue Variable und die ursprüngliche Variable enthalten daher zwei separate Kopien derselben Daten. An einer Kopie vorgenommene Änderungen wirken sich nicht auf die andere Kopie aus.

Im Allgemeinen werden classes verwendet, um komplexeres Verhalten oder Daten zu modellieren, die geändert werden sollen, nachdem ein class -Objekt erstellt wurde. Structs eignet sich am besten für kleine Datenstrukturen, die hauptsächlich Daten enthalten, die nach dem Erstellen von struct nicht mehr geändert werden sollen.

für mehr ...

2
Sujit

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.

1
bashmohandes

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

Structs

Darüber hinaus reservieren Klassen, wenn sie in .NET instanziiert werden, Speicher auf dem Heap oder dem reservierten Speicherplatz von .NET. Während Strukturen aufgrund der Zuweisung auf dem Stapel effizienter sind, wenn sie instanziiert werden. Außerdem sollte beachtet werden, dass die Übergabe von Parametern innerhalb von Strukturen nach Wert erfolgt.

1
DaveK