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 new
Operator 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 new
Operator 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 stfld
und 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 new
Aufrufen 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, new
Speicherplatz 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 new
Aufrufe - 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 guid
nicht sichtbar, wenn der Guid
Konstruktor 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!
List<Guid>
diese 3 haben und hinzufügen? Das wären 3 Zuweisungen (gleiche IL)? Aber sie werden an einem magischen Ort aufbewahrtguid
nur zur Hälfte überschrieben wurde, da er ohnehin nicht sichtbar ist.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).
quelle
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 #.
quelle
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 .
quelle
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]
quelle
Eine
class
oderstruct
-Deklaration ist wie eine Blaupause, mit der Instanzen oder Objekte zur Laufzeit erstellt werden. Wenn Sie eineclass
oder einestruct
angerufene 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 ihremproperties
und habenfields
.A
class
ist ein Referenztyp. Wenn ein Objekt vonclass
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. Über eine Variable vorgenommene Änderungen werden in der anderen Variablen wiedergegeben, da beide auf dieselben Daten verweisen.A
struct
ist ein Werttyp. Wenn astruct
erstellt wird, enthält die Variable, der dasstruct
zugewiesen ist, die tatsächlichen Daten der Struktur. Wenn dasstruct
einer 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
classes
werden sie verwendet, um komplexeres Verhalten oder Daten zu modellieren, die nach dem Erstellen einesclass
Objekts geändert werden sollen .Structs
sind am besten für kleine Datenstrukturen geeignet, die hauptsächlich Daten enthalten, die nach der Erstellung nicht geändert werden sollenstruct
.für mehr...
quelle
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.
quelle
Strukturen werden dem Stapel zugeordnet. Hier ist eine hilfreiche Erklärung:
Strukturen
quelle