Podczas tworzenia wystąpienia klasy z new
operatorem pamięć jest przydzielana na stercie. Kiedy tworzysz wystąpienie struktury z new
operatorem, gdzie przydzielana jest pamięć, na stercie lub na stosie?
źródło
Podczas tworzenia wystąpienia klasy z new
operatorem pamięć jest przydzielana na stercie. Kiedy tworzysz wystąpienie struktury z new
operatorem, gdzie przydzielana jest pamięć, na stercie lub na stosie?
Dobra, zobaczmy, czy mogę to wyjaśnić.
Po pierwsze, Ash ma rację: pytanie nie dotyczy tego, gdzie przydzielane są zmienne typu wartości . To inne pytanie - na które odpowiedź nie polega tylko na „stosie”. To jest bardziej skomplikowane (i jeszcze bardziej skomplikowane przez C # 2). Mam artykuł na ten temat i rozwinę go na żądanie, ale porozmawiajmy tylko z new
operatorem.
Po drugie, wszystko to naprawdę zależy od poziomu, o którym mówisz. Patrzę na to, co kompilator robi z kodem źródłowym, pod względem tworzonej IL. Jest więcej niż możliwe, że kompilator JIT zrobi sprytne rzeczy, jeśli chodzi o zoptymalizowanie całkiem sporo „logicznej” alokacji.
Po trzecie, ignoruję generyczne, głównie dlatego, że tak naprawdę nie znam odpowiedzi, a częściowo dlatego, że skomplikowałoby to wszystko zbytnio.
Wreszcie, wszystko to odbywa się w ramach obecnego wdrożenia. Specyfikacja C # nie precyzuje wiele z tego - to faktycznie szczegół implementacji. Są tacy, którzy uważają, że twórcy kodu zarządzanego naprawdę nie powinni się przejmować. Nie jestem pewien, czy posunę się tak daleko, ale warto wyobrazić sobie świat, w którym w rzeczywistości wszystkie lokalne zmienne żyją na stercie - co nadal byłoby zgodne ze specyfikacją.
Istnieją dwie różne sytuacje z new
operatorem dla typów wartości: możesz wywołać konstruktora bez parametrów (np. new Guid()
) Lub konstruktora parametrycznego (np new Guid(someString)
.). Generują one znacząco różne IL. Aby zrozumieć, dlaczego, musisz porównać specyfikacje C # i CLI: zgodnie z C # wszystkie typy wartości mają konstruktor bez parametrów. Zgodnie ze specyfikacją CLI żadne typy wartości nie mają konstruktorów bez parametrów. (Pobierz konstruktory typu wartości z odbiciem jakiś czas - nie znajdziesz parametru bez parametrów).
C # ma sens traktować „inicjowanie wartości zerami” jako konstruktor, ponieważ utrzymuje spójność języka - możesz myśleć o tym, new(...)
że zawsze wywołujesz konstruktor. CLI ma sens, aby myśleć o tym inaczej, ponieważ nie ma prawdziwego kodu do wywołania - a na pewno nie ma kodu specyficznego dla typu.
Ma również znaczenie to, co zrobisz z wartością po jej zainicjowaniu. IL używany do
Guid localVariable = new Guid(someString);
różni się od IL zastosowanej do:
myInstanceOrStaticVariable = new Guid(someString);
Ponadto, jeśli wartość jest używana jako wartość pośrednia, np. Jako argument wywołania metody, sytuacja znów się nieco różni. Aby pokazać wszystkie te różnice, oto krótki program testowy. Nie pokazuje różnicy między zmiennymi statycznymi a zmiennymi instancji: IL różni się między stfld
i stsfld
, ale to wszystko.
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());
}
}
Oto IL dla klasy, z wyłączeniem nieistotnych bitów (takich jak 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
}
Jak widać, istnieje wiele różnych instrukcji służących do wywoływania konstruktora:
newobj
: Przydziela wartość na stosie, wywołuje sparametryzowany konstruktor. Używany do wartości pośrednich, np. Do przypisania do pola lub jako argument metody.call instance
: Używa już przydzielonej lokalizacji pamięci (na stosie lub nie). Jest to użyte w powyższym kodzie do przypisania do zmiennej lokalnej. Jeśli ta sama zmienna lokalna zostanie kilkakrotnie przypisana do wartości za pomocą kilku new
wywołań, po prostu inicjuje dane ponad starą wartością - za każdym razem nie przydziela więcej miejsca na stosie.initobj
: Używa już przydzielonej lokalizacji pamięci i po prostu czyści dane. Jest to wykorzystywane do wszystkich naszych bezparametrowych wywołań konstruktora, w tym tych, które przypisują zmienną lokalną. W wywołaniu metody efektywnie wprowadza się pośrednią zmienną lokalną, a jej wartość usuwa się initobj
.Mam nadzieję, że to pokazuje, jak skomplikowany jest ten temat, jednocześnie rzucając na niego nieco światła. W niektórych koncepcyjnych sensach każde wezwanie do new
przydzielenia miejsca na stosie - ale jak widzieliśmy, tak naprawdę nie dzieje się nawet na poziomie IL. Chciałbym podkreślić jeden konkretny przypadek. Weź tę metodę:
void HowManyStackAllocations()
{
Guid guid = new Guid();
// [...] Use guid
guid = new Guid(someBytes);
// [...] Use guid
guid = new Guid(someString);
// [...] Use guid
}
To „logicznie” ma 4 przydziały stosu - po jednym dla zmiennej i po jednym dla każdego z trzech new
wywołań - ale w rzeczywistości (dla tego konkretnego kodu) stos jest przydzielany tylko raz, a następnie ta sama lokalizacja pamięci jest ponownie wykorzystywana.
EDYCJA: Żeby było jasne, jest to prawdą tylko w niektórych przypadkach ... w szczególności wartość guid
nie będzie widoczna, jeśli Guid
konstruktor zgłosi wyjątek, dlatego kompilator C # jest w stanie ponownie użyć tego samego miejsca na stosie. Zobacz post na blogu Erica Lipperta o konstrukcji typu wartości, aby uzyskać więcej szczegółów i przypadek, w którym nie ma zastosowania.
Nauczyłem się wiele, pisząc tę odpowiedź - poproś o wyjaśnienie, jeśli coś jest niejasne!
List<Guid>
dodasz i dodasz 3? To byłyby 3 przydziały (ta sama IL)? Ale są trzymane gdzieś magicznieguid
została tylko częściowo nadpisana, ponieważ i tak nie będzie widoczna.Pamięć zawierająca pola struktury może być przydzielona albo na stosie, albo na stosie, w zależności od okoliczności. Jeśli zmienna typu struct jest zmienną lokalną lub parametrem, który nie jest przechwytywany przez anonimową klasę delegata lub iteratora, to zostanie ona przydzielona na stosie. Jeśli zmienna jest częścią jakiejś klasy, to zostanie przydzielona w klasie na stercie.
Jeśli struktura jest przydzielona na stercie, wywołanie nowego operatora nie jest w rzeczywistości konieczne do przydzielenia pamięci. Jedynym celem byłoby ustawienie wartości pola zgodnie z tym, co jest w konstruktorze. Jeśli konstruktor nie zostanie wywołany, wszystkie pola otrzymają wartości domyślne (0 lub null).
Podobnie dla struktur przydzielonych na stosie, z tym wyjątkiem, że C # wymaga, aby wszystkie zmienne lokalne były ustawione na pewną wartość przed ich użyciem, więc musisz wywołać konstruktor niestandardowy lub domyślny (konstruktor, który nie przyjmuje parametrów, jest zawsze dostępny dla struktury).
źródło
Mówiąc krótko, new jest mylącą nazwą dla struktur, wywoływanie new po prostu wywołuje konstruktor. Jedyną lokalizacją pamięci dla struktury jest lokalizacja, w której jest zdefiniowana.
Jeśli jest to zmienna składowa, jest ona przechowywana bezpośrednio w dowolnej definicji, jeśli jest zmienną lokalną lub parametrem, jest przechowywana na stosie.
Porównaj to z klasami, które mają odniesienie wszędzie tam, gdzie struktura byłaby przechowywana w całości, podczas gdy odniesienie wskazuje gdzieś na stercie. (Członek wewnątrz, lokalny / parametr na stosie)
Pomocne może być spojrzenie na C ++, gdzie nie ma rzeczywistego rozróżnienia między klasą / strukturą. (W języku są podobne nazwy, ale odnoszą się one tylko do domyślnej dostępności rzeczy). Kiedy wywołujesz new, dostajesz wskaźnik do lokalizacji stosu, a jeśli masz odwołanie inne niż wskaźnik, jest ono przechowywane bezpośrednio na stosie lub w drugim obiekcie ala structs w C #.
źródło
Podobnie jak w przypadku wszystkich typów wartości, struktury zawsze idą tam, gdzie zostały zadeklarowane .
Zobacz to pytanie tutaj, aby uzyskać więcej informacji o tym, kiedy używać struktur. I to pytanie tutaj aby uzyskać więcej informacji na temat struktur.
Edycja: błędnie odpowiedziałem, że ZAWSZE wchodzą na stos. To jest błędne .
źródło
Prawdopodobnie coś tu brakuje, ale dlaczego zależy nam na alokacji?
Typy wartości są przekazywane przez wartość;) i dlatego nie można ich mutować w innym zakresie niż tam, gdzie są zdefiniowane. Aby móc mutować wartość, musisz dodać słowo kluczowe [ref].
Typy referencyjne są przekazywane przez referencję i mogą być mutowane.
Oczywiście są najbardziej niezmienne ciągi typów referencyjnych.
Układ / inicjalizacja tablicy: Typy wartości -> zerowa pamięć [nazwa, zip] [nazwa, zip] Typy referencyjne -> zerowa pamięć -> null [ref] [ref]
źródło
class
Lubstruct
deklaracja jest jak plan, który jest używany do utworzenia instancji lub przedmiotów w czasie wykonywania. Jeśli zdefiniujeszclass
lub ostruct
nazwie Osoba, Osoba jest nazwą typu. Jeśli zadeklarujesz i zainicjujesz zmienną p typu Person, p zostanie uznane za obiekt lub instancję Person. Można utworzyć wiele wystąpień tego samego typu Osoba, a każde wystąpienie może mieć różne wartości w swoimproperties
ifields
.A
class
jest typem odniesienia. Gdy obiektclass
zmiennej zmienna, do której obiekt jest przypisany, zawiera tylko odwołanie do tej pamięci. Gdy odwołanie do obiektu jest przypisane do nowej zmiennej, nowa zmienna odnosi się do oryginalnego obiektu. Zmiany dokonane za pomocą jednej zmiennej są odzwierciedlone w drugiej zmiennej, ponieważ obie odnoszą się do tych samych danych.A
struct
jest typem wartości. Kiedystruct
tworzony jest a , zmienna, do którejstruct
jest przypisany, przechowuje rzeczywiste dane struktury. Postruct
przypisaniu do nowej zmiennej jest ona kopiowana. Nowa zmienna i pierwotna zmienna zawierają zatem dwie osobne kopie tych samych danych. Zmiany wprowadzone w jednej kopii nie wpływają na drugą kopię.Zasadniczo
classes
służą do modelowania bardziej złożonych zachowań lub danych, które mają zostać zmodyfikowane po utworzeniuclass
obiektu.Structs
najlepiej nadają się do małych struktur danych, które zawierają przede wszystkim dane, które nie są modyfikowane postruct
utworzeniu.po więcej ...
źródło
Prawie struktury, które są uważane za typy wartości, są przydzielane na stosie, podczas gdy obiekty są przydzielane na stercie, podczas gdy odniesienie do obiektu (wskaźnik) jest przydzielane na stosie.
źródło
Struktury są przydzielane do stosu. Oto pomocne wyjaśnienie:
Struktury
źródło