Saat Anda membuat instance kelas dengan new
operator, memori akan dialokasikan pada heap. Ketika Anda membuat instance dari struct dengan new
operator di mana memori dialokasikan, di tumpukan atau di tumpukan?
Saat Anda membuat instance kelas dengan new
operator, memori akan dialokasikan pada heap. Ketika Anda membuat instance dari struct dengan new
operator di mana memori dialokasikan, di tumpukan atau di tumpukan?
Jawaban:
Oke, mari kita lihat apakah saya bisa memperjelas ini.
Pertama, Ash benar: pertanyaannya bukan tentang di mana variabel tipe nilai dialokasikan. Itu pertanyaan yang berbeda - dan yang jawabannya tidak hanya "di tumpukan". Ini lebih rumit dari itu (dan dibuat lebih rumit oleh C # 2). Saya memiliki artikel tentang topik ini dan akan diperluas jika diminta, tetapi mari kita berurusan dengan new
operator saja.
Kedua, semua ini sangat tergantung pada level apa yang Anda bicarakan. Saya melihat apa yang dilakukan kompiler dengan kode sumber, dalam hal IL yang dibuatnya. Lebih dari kemungkinan bahwa kompiler JIT akan melakukan hal-hal pintar dalam hal mengoptimalkan alokasi "logis" yang cukup banyak.
Ketiga, saya mengabaikan obat generik, kebanyakan karena saya sebenarnya tidak tahu jawabannya, dan sebagian lagi karena terlalu rumit.
Akhirnya, semua ini hanya dengan implementasi saat ini. C # spec tidak menentukan banyak tentang hal ini - ini adalah detail implementasi yang efektif. Ada orang yang percaya bahwa pengembang kode terkelola benar-benar tidak peduli. Saya tidak yakin saya akan melangkah sejauh itu, tapi ada baiknya membayangkan sebuah dunia di mana sebenarnya semua variabel lokal tinggal di heap - yang masih akan sesuai dengan spesifikasi.
Ada dua situasi berbeda dengan new
operator pada tipe nilai: Anda dapat memanggil konstruktor tanpa parameter (misalnya new Guid()
) atau konstruktor penuh parameter (misalnya new Guid(someString)
). Ini menghasilkan IL yang sangat berbeda. Untuk memahami alasannya, Anda perlu membandingkan spesifikasi C # dan CLI: menurut C #, semua tipe nilai memiliki konstruktor tanpa parameter. Menurut spesifikasi CLI, tidak ada tipe nilai yang memiliki konstruktor tanpa parameter. (Ambil konstruktor dari tipe nilai dengan refleksi beberapa waktu - Anda tidak akan menemukan yang tanpa parameter.)
Masuk akal bagi C # untuk memperlakukan "inisialisasi nilai dengan nol" sebagai konstruktor, karena itu membuat bahasa tetap konsisten - Anda dapat menganggapnya new(...)
sebagai selalu memanggil konstruktor. Masuk akal bagi CLI untuk memikirkannya secara berbeda, karena tidak ada kode nyata untuk dipanggil - dan tentu saja tidak ada kode jenis khusus.
Ini juga membuat perbedaan apa yang akan Anda lakukan dengan nilai setelah Anda menginisialisasi. IL digunakan untuk
Guid localVariable = new Guid(someString);
berbeda dengan IL yang digunakan untuk:
myInstanceOrStaticVariable = new Guid(someString);
Selain itu, jika nilai tersebut digunakan sebagai nilai perantara, misalnya argumen untuk pemanggilan metode, hal-hal sedikit berbeda lagi. Untuk menunjukkan semua perbedaan ini, inilah program tes singkat. Itu tidak menunjukkan perbedaan antara variabel statis dan variabel instan: IL akan berbeda antara stfld
dan stsfld
, tapi itu saja.
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());
}
}
Inilah IL untuk kelas, tidak termasuk bit yang tidak relevan (seperti 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
}
Seperti yang Anda lihat, ada banyak instruksi berbeda yang digunakan untuk memanggil konstruktor:
newobj
: Mengalokasikan nilai pada stack, memanggil konstruktor parameterised. Digunakan untuk nilai menengah, misalnya untuk penugasan ke bidang atau digunakan sebagai argumen metode.call instance
: Menggunakan lokasi penyimpanan yang sudah dialokasikan (baik di tumpukan atau tidak). Ini digunakan dalam kode di atas untuk menetapkan ke variabel lokal. Jika variabel lokal yang sama diberikan nilai beberapa kali menggunakan beberapa new
panggilan, itu hanya menginisialisasi data di atas nilai lama - itu tidak mengalokasikan lebih banyak ruang stack setiap kali.initobj
: Menggunakan lokasi penyimpanan yang sudah dialokasikan dan hanya menghapus data. Ini digunakan untuk semua panggilan konstruktor tanpa parameter kami, termasuk yang menetapkan untuk variabel lokal. Untuk pemanggilan metode, variabel lokal perantara diperkenalkan secara efektif, dan nilainya dihapus oleh initobj
.Saya harap ini menunjukkan betapa rumitnya topik itu, sambil menyinari sedikit cahaya pada saat yang bersamaan. Dalam beberapa pengertian konseptual, setiap panggilan untuk new
mengalokasikan ruang pada stack - tetapi seperti yang telah kita lihat, itu bukanlah yang sebenarnya terjadi bahkan pada level IL. Saya ingin menyoroti satu kasus khusus. Ambil metode ini:
void HowManyStackAllocations()
{
Guid guid = new Guid();
// [...] Use guid
guid = new Guid(someBytes);
// [...] Use guid
guid = new Guid(someString);
// [...] Use guid
}
Bahwa "secara logis" memiliki 4 alokasi stack - satu untuk variabel, dan satu untuk masing-masing dari tiga new
panggilan - tetapi pada kenyataannya (untuk kode spesifik itu) stack hanya dialokasikan satu kali, dan kemudian lokasi penyimpanan yang sama digunakan kembali.
EDIT: Hanya untuk memperjelas, ini hanya benar dalam beberapa kasus ... khususnya, nilai guid
tidak akan terlihat jika Guid
konstruktor melempar pengecualian, itulah sebabnya kompiler C # dapat menggunakan kembali slot stack yang sama. Lihat posting blog Eric Lippert tentang konstruksi tipe nilai untuk detail lebih lanjut dan kasus di mana itu tidak berlaku.
Saya telah belajar banyak dalam menulis jawaban ini - silakan minta klarifikasi jika ada yang tidak jelas!
List<Guid>
dan menambahkan 3 itu? Itu akan menjadi 3 alokasi (IL yang sama)? Tapi mereka disimpan di suatu tempat yang ajaib
guid
hanya setengah ditimpa, karena tidak akan terlihat.
Memori yang berisi bidang struct dapat dialokasikan pada tumpukan atau tumpukan tergantung pada keadaan. Jika variabel tipe-struktural adalah variabel lokal atau parameter yang tidak ditangkap oleh beberapa delegasi anonim atau kelas iterator, maka itu akan dialokasikan pada stack. Jika variabel adalah bagian dari beberapa kelas, maka itu akan dialokasikan di dalam kelas di heap.
Jika struct dialokasikan pada heap, maka memanggil operator baru sebenarnya tidak perlu mengalokasikan memori. Satu-satunya tujuan adalah untuk menetapkan nilai bidang sesuai dengan apa pun yang ada di konstruktor. Jika konstruktor tidak dipanggil, maka semua bidang akan mendapatkan nilai defaultnya (0 atau nol).
Demikian pula untuk struct yang dialokasikan pada stack, kecuali bahwa C # mengharuskan semua variabel lokal diatur ke beberapa nilai sebelum mereka digunakan, jadi Anda harus memanggil konstruktor kustom atau konstruktor default (konstruktor yang tidak mengambil parameter selalu tersedia untuk struct).
Singkatnya, baru adalah keliru untuk struct, memanggil baru hanya memanggil konstruktor. Satu-satunya lokasi penyimpanan untuk struct adalah lokasi yang ditentukan.
Jika itu adalah variabel anggota, ia disimpan secara langsung dalam apa pun yang didefinisikan, jika itu adalah variabel lokal atau parameter itu disimpan di tumpukan.
Bandingkan ini dengan kelas, yang memiliki referensi di mana pun struct akan disimpan secara keseluruhan, sementara referensi menunjuk ke suatu tempat di heap. (Anggota dalam, lokal / parameter pada tumpukan)
Mungkin membantu untuk melihat sedikit ke dalam C ++, di mana tidak ada perbedaan nyata antara kelas / struct. (Ada nama-nama yang mirip dalam bahasa, tetapi mereka hanya merujuk pada aksesibilitas default hal-hal) Ketika Anda menelepon baru Anda mendapatkan pointer ke lokasi tumpukan, sementara jika Anda memiliki referensi non-pointer itu disimpan langsung di tumpukan atau dalam objek lain, ala struct di C #.
Seperti dengan semua jenis nilai, struct selalu pergi ke tempat yang dideklarasikan .
Lihat pertanyaan ini di sini untuk detail lebih lanjut tentang kapan harus menggunakan struct. Dan pertanyaan ini di sini untuk info lebih lanjut tentang struct.
Sunting: Saya telah salah menjawab bahwa mereka SELALU masuk ke tumpukan. Ini salah .
Saya mungkin melewatkan sesuatu di sini, tetapi mengapa kita peduli tentang alokasi?
Jenis nilai dilewatkan oleh nilai;) dan karenanya tidak dapat dimutasi pada ruang lingkup yang berbeda dari tempat mereka didefinisikan. Untuk dapat mengubah nilai Anda harus menambahkan kata kunci [ref].
Jenis referensi dilewatkan oleh referensi dan dapat dimutasi.
Tentu saja ada string tipe referensi yang tidak berubah menjadi yang paling populer.
Tata letak / inisialisasi array: Tipe nilai -> zero memory [nama, zip] [nama, zip] Jenis referensi -> zero memory -> null [ref] [ref]
Pernyataan class
atau struct
deklarasi seperti cetak biru yang digunakan untuk membuat instance atau objek pada saat run time. Jika Anda mendefinisikan class
atau struct
disebut Person, Person adalah nama dari tipe tersebut. Jika Anda mendeklarasikan dan menginisialisasi p variabel tipe Person, p dikatakan sebagai objek atau instance Person. Beberapa instance dari tipe Person yang sama dapat dibuat, dan setiap instance dapat memiliki nilai yang berbeda di dalamnya properties
dan fields
.
A class
adalah tipe referensi. Ketika sebuah objek class
dibuat, variabel yang objek ditugaskan hanya memiliki referensi ke memori itu. Ketika referensi objek ditugaskan ke variabel baru, variabel baru merujuk ke objek asli. Perubahan yang dilakukan melalui satu variabel tercermin dalam variabel lain karena keduanya merujuk pada data yang sama.
A struct
adalah tipe nilai. Ketika a struct
dibuat, variabel yang struct
ditugaskan menyimpan data aktual struct. Ketika struct
ditugaskan ke variabel baru, itu akan disalin. Variabel baru dan variabel asli karenanya mengandung dua salinan terpisah dari data yang sama. Perubahan yang dilakukan pada satu salinan tidak memengaruhi salinan lainnya.
Secara umum, classes
digunakan untuk memodelkan perilaku yang lebih kompleks, atau data yang dimaksudkan untuk dimodifikasi setelah class
objek dibuat. Structs
paling cocok untuk struktur data kecil yang terutama berisi data yang tidak dimaksudkan untuk dimodifikasi setelah struct
dibuat.
Cukup banyak struct yang dianggap tipe Value, dialokasikan pada stack, sementara objek dialokasikan pada heap, sedangkan referensi objek (pointer) akan dialokasikan pada stack.
Structs dialokasikan ke stack. Berikut ini penjelasan yang bermanfaat:
Selain itu, kelas ketika dipakai dalam. NET mengalokasikan memori pada heap atau ruang memori yang disediakan .NET. Sedangkan struct menghasilkan lebih banyak efisiensi ketika instantiated karena alokasi pada stack. Lebih lanjut, harus dicatat bahwa melewati parameter dalam struct dilakukan dengan nilai.