เมื่อคุณสร้างอินสแตนซ์ของคลาสด้วยnew
โอเปอเรเตอร์หน่วยความจำจะได้รับการจัดสรรบนฮีป เมื่อคุณสร้างอินสแตนซ์ของโครงสร้างด้วยตัวnew
ดำเนินการที่หน่วยความจำได้รับการจัดสรรบนกองหรือกอง?
เมื่อคุณสร้างอินสแตนซ์ของคลาสด้วยnew
โอเปอเรเตอร์หน่วยความจำจะได้รับการจัดสรรบนฮีป เมื่อคุณสร้างอินสแตนซ์ของโครงสร้างด้วยตัวnew
ดำเนินการที่หน่วยความจำได้รับการจัดสรรบนกองหรือกอง?
คำตอบ:
เอาล่ะมาดูกันว่าฉันจะทำให้เรื่องนี้ชัดเจนขึ้นได้ไหม
ประการแรกแอชพูดถูก: คำถามไม่เกี่ยวกับการจัดสรรตัวแปรประเภทค่า นั่นเป็นคำถามที่แตกต่าง - และอีกหนึ่งคำถามที่คำตอบไม่ใช่แค่ "ในสแต็ก" มันซับซ้อนกว่านั้น (และทำให้ซับซ้อนยิ่งขึ้นด้วย C # 2) ฉันมีบทความเกี่ยวกับหัวข้อและจะขยายออกไปหากมีการร้องขอ แต่ให้จัดการกับnew
ผู้ดำเนินการ
ประการที่สองทั้งหมดนี้ขึ้นอยู่กับระดับที่คุณพูดถึง ฉันกำลังดูว่าคอมไพเลอร์ทำอะไรกับซอร์สโค้ดในแง่ของ IL ที่มันสร้างขึ้น มันเป็นไปได้มากกว่าที่คอมไพเลอร์ JIT จะทำสิ่งที่ฉลาดในแง่ของการเพิ่มประสิทธิภาพการจัดสรรแบบ "ตรรกะ" ค่อนข้างมาก
ประการที่สามฉันไม่สนใจยาชื่อสามัญส่วนใหญ่เป็นเพราะฉันไม่รู้คำตอบจริง ๆ และส่วนหนึ่งเพราะมันซับซ้อนเกินไป
ในที่สุดทั้งหมดนี้เป็นเพียงการใช้งานในปัจจุบัน ข้อมูลจำเพาะ C # ไม่ได้ระบุสิ่งนี้มากนัก - มันเป็นรายละเอียดการติดตั้งใช้งานที่มีประสิทธิภาพ มีผู้ที่เชื่อว่านักพัฒนารหัสที่ได้รับการจัดการไม่ควรสนใจ ฉันไม่แน่ใจว่าฉันจะไปไกลขนาดนั้น แต่มันก็คุ้มค่าที่จะจินตนาการถึงโลกที่ที่จริงแล้วตัวแปรท้องถิ่นทั้งหมดอาศัยอยู่ในกอง - ซึ่งจะยังคงสอดคล้องกับข้อมูลจำเพาะ
มีสองสถานการณ์ที่แตกต่างกันกับnew
โอเปอเรเตอร์กับประเภทค่า: คุณสามารถเรียกตัวสร้างแบบไร้พารามิเตอร์ (เช่นnew Guid()
) หรือตัวสร้างแบบพารามิเตอร์ (เช่นnew Guid(someString)
) สิ่งเหล่านี้สร้าง IL ที่แตกต่างกันอย่างมีนัยสำคัญ เพื่อให้เข้าใจว่าทำไมคุณต้องเปรียบเทียบข้อมูลจำเพาะ C # และ CLI: ตาม C # ประเภทค่าทั้งหมดมีตัวสร้างแบบไม่มีพารามิเตอร์ ตามข้อกำหนด CLI ไม่มีชนิดค่าที่มีตัวสร้างแบบไม่มีพารามิเตอร์ (เรียกคอนสตรัคเตอร์ของประเภทค่าที่มีการสะท้อนบางครั้ง - คุณจะไม่พบพารามิเตอร์ที่ไม่มีพารามิเตอร์)
มันทำให้รู้สึกสำหรับ C # ในการรักษา "เริ่มต้นด้วยเลขศูนย์" เป็นตัวสร้างเพราะจะช่วยให้ภาษาที่สอดคล้องกัน - คุณสามารถคิดnew(...)
เป็นเสมอโทรนวกรรมิก มันสมเหตุสมผลสำหรับ CLI ที่จะคิดแตกต่างเนื่องจากไม่มีรหัสจริงที่จะโทร - และแน่นอนว่าไม่มีรหัสเฉพาะประเภท
นอกจากนี้ยังสร้างความแตกต่างในสิ่งที่คุณจะทำกับค่าหลังจากที่คุณเริ่มต้นมัน IL ใช้สำหรับ
Guid localVariable = new Guid(someString);
ต่างจาก IL ที่ใช้สำหรับ:
myInstanceOrStaticVariable = new Guid(someString);
นอกจากนี้หากใช้ค่าเป็นค่ากลางเช่นการโต้แย้งในการเรียกใช้เมธอดสิ่งต่าง ๆ อีกเล็กน้อย เพื่อแสดงความแตกต่างเหล่านี้นี่คือโปรแกรมทดสอบสั้น ๆ ไม่แสดงความแตกต่างระหว่างตัวแปรสแตติกและตัวแปรอินสแตนซ์: IL จะแตกต่างกันระหว่างstfld
และstsfld
แต่นั่นคือทั้งหมด
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());
}
}
นี่คือ IL สำหรับชั้นเรียนยกเว้นส่วนที่ไม่เกี่ยวข้อง (เช่น 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
}
อย่างที่คุณเห็นมีคำสั่งต่าง ๆ มากมายที่ใช้เรียกตัวสร้าง:
newobj
: จัดสรรค่าบนสแต็กเรียกตัวสร้างพารามิเตอร์ ใช้สำหรับค่ากลางเช่นการกำหนดให้กับเขตข้อมูลหรือใช้เป็นอาร์กิวเมนต์วิธีcall instance
: ใช้ที่เก็บข้อมูลที่จัดสรรไว้แล้ว (ไม่ว่าจะอยู่ในสแต็กหรือไม่ก็ตาม) ใช้ในรหัสด้านบนเพื่อกำหนดให้กับตัวแปรท้องถิ่น หากตัวแปรโลคัลเดียวกันถูกกำหนดค่าหลายครั้งโดยใช้การnew
เรียกหลายครั้งมันเพิ่งเริ่มต้นข้อมูลที่ด้านบนของค่าเดิม - มันจะไม่จัดสรรพื้นที่สแต็กเพิ่มเติมในแต่ละครั้งinitobj
: ใช้ที่เก็บข้อมูลที่จัดสรรไว้แล้วและเพียงแค่ลบข้อมูล สิ่งนี้ใช้สำหรับการเรียกพารามิเตอร์คอนสตรัคเตอร์แบบไร้พารามิเตอร์ทั้งหมดของเรารวมถึงสิ่งที่กำหนดให้กับตัวแปรโลคอล initobj
สำหรับวิธีการเรียกเป็นตัวแปรท้องถิ่นกลางเป็นที่รู้จักได้อย่างมีประสิทธิภาพและคุ้มค่าโดยเช็ดฉันหวังว่าสิ่งนี้จะแสดงให้เห็นว่าหัวข้อซับซ้อนเพียงใดในขณะที่ส่องแสงเล็กน้อยในเวลาเดียวกัน ในประสาทสัมผัสบางแนวคิดทุกการเรียกเพื่อnew
จัดสรรพื้นที่บนสแต็ก - แต่อย่างที่เราได้เห็นนั่นไม่ใช่สิ่งที่เกิดขึ้นจริงแม้แต่ในระดับ IL ฉันต้องการเน้นเฉพาะกรณี ใช้วิธีนี้:
void HowManyStackAllocations()
{
Guid guid = new Guid();
// [...] Use guid
guid = new Guid(someBytes);
// [...] Use guid
guid = new Guid(someString);
// [...] Use guid
}
"ตรรกะ" นั้นมี 4 การจัดสรรสแต็ก - หนึ่งสำหรับตัวแปรและอีกหนึ่งสำหรับแต่ละการnew
เรียกสามครั้ง- แต่อันที่จริง (สำหรับรหัสเฉพาะนั้น) สแต็กจะถูกจัดสรรเพียงครั้งเดียวและจากนั้นจะใช้ที่เก็บข้อมูลเดียวกัน
แก้ไข: เพื่อให้ชัดเจนนี่เป็นจริงเฉพาะในบางกรณี ... โดยเฉพาะค่าguid
จะไม่ปรากฏหากGuid
Constructor ส่งข้อยกเว้นซึ่งเป็นสาเหตุที่คอมไพเลอร์ C # สามารถนำสแต็กสล็อตเดียวกันกลับมาใช้ใหม่ได้ ดูเอริค Lippert ของบล็อกโพสต์เกี่ยวกับประเภทค่าก่อสร้างสำหรับรายละเอียดเพิ่มเติมและกรณีที่มันไม่ได้ใช้
ฉันได้เรียนรู้มากมายในการเขียนคำตอบนี้ - โปรดขอคำอธิบายหากมีข้อใดไม่ชัดเจน!
List<Guid>
และเพิ่ม 3 ตัวนั้นเข้าไป? นั่นจะเป็นการจัดสรร 3 ครั้ง (IL เดียวกัน) แต่พวกเขาก็ยังคงมีมนต์ขลังอยู่
guid
การถูกเขียนทับเพียงครึ่งเดียวเนื่องจากไม่สามารถมองเห็นได้
หน่วยความจำที่มีเขตข้อมูลของ struct สามารถจัดสรรในกองหรือกองขึ้นอยู่กับสถานการณ์ ถ้าตัวแปร struct-type เป็นตัวแปรโลคัลหรือพารามิเตอร์ที่ไม่ถูกดักจับโดยผู้ร่วมประชุมที่ไม่ระบุชื่อหรือคลาสตัววนซ้ำแล้วจะถูกจัดสรรบนสแต็ก หากตัวแปรนั้นเป็นส่วนหนึ่งของคลาสบางคลาสมันจะถูกจัดสรรภายในคลาสบนฮีป
หากมีการจัดสรรโครงสร้างบนฮีปการเรียกผู้ดำเนินการใหม่นั้นไม่จำเป็นต้องจัดสรรหน่วยความจำ จุดประสงค์เดียวก็เพื่อตั้งค่าฟิลด์ตามสิ่งที่อยู่ในตัวสร้าง หากไม่ได้เรียกตัวสร้างแล้วเขตข้อมูลทั้งหมดจะได้รับค่าเริ่มต้น (0 หรือ null)
เช่นเดียวกันกับ structs ที่จัดสรรไว้ใน stack ยกเว้นว่า C # ต้องการตัวแปรท้องถิ่นทั้งหมดเพื่อตั้งค่าบางค่าก่อนที่จะใช้ดังนั้นคุณต้องเรียกใช้ตัวสร้างแบบกำหนดเองหรือตัวสร้างเริ่มต้น (ตัวสร้างที่ไม่มีพารามิเตอร์จะพร้อมใช้งานเสมอ structs)
ในการทำให้กะทัดรัดมันใหม่คือการเรียกชื่อผิดสำหรับ structs การเรียกใหม่นั้นเรียกว่านวกรรมิก ตำแหน่งหน่วยเก็บข้อมูลเดียวสำหรับ struct คือตำแหน่งที่ถูกกำหนด
ถ้ามันเป็นตัวแปรสมาชิกมันจะถูกเก็บไว้โดยตรงในสิ่งที่มันถูกกำหนดไว้ในถ้ามันเป็นตัวแปรท้องถิ่นหรือพารามิเตอร์มันจะถูกเก็บไว้ในกอง
เปรียบเทียบสิ่งนี้กับคลาสซึ่งมีการอ้างอิงทุกที่ที่โครงสร้างจะถูกเก็บไว้อย่างครบถ้วนในขณะที่การอ้างอิงชี้ไปที่ที่ใดที่หนึ่งบนฮีป (สมาชิกภายในท้องถิ่น / พารามิเตอร์บนสแต็ก)
มันอาจช่วยให้มองเข้าไปใน C ++ ซึ่งไม่มีความแตกต่างระหว่าง class / struct (มีชื่อที่คล้ายกันในภาษา แต่พวกเขาอ้างถึงการเข้าใช้งานเริ่มต้นของสิ่งต่าง ๆ เท่านั้น) เมื่อคุณโทรหาใหม่คุณจะได้รับตัวชี้ไปยังที่ตั้งฮีปในขณะที่ถ้าคุณมีการอ้างอิงที่ไม่ใช่ตัวชี้ ภายในวัตถุอื่น ๆ ala structs ใน C #
เช่นเดียวกับประเภทค่าทั้งหมด structs จะไปยังตำแหน่งที่ถูกประกาศเสมอ
ดูคำถามนี้ ที่นี่สำหรับรายละเอียดเพิ่มเติมเกี่ยวกับเวลาที่จะใช้ structs และคำถามนี้ที่นี่สำหรับข้อมูลเพิ่มเติมเกี่ยวกับ structs
แก้ไข:ฉันได้ตอบ mistankely ว่าพวกเขาเสมอไปในกอง นี่คือที่ไม่ถูกต้อง
ฉันอาจจะพลาดอะไรบางอย่างที่นี่ แต่ทำไมเราถึงสนใจการจัดสรร
ประเภทค่าถูกส่งผ่านโดยค่า;) ดังนั้นจึงไม่สามารถเปลี่ยนแปลงที่ขอบเขตที่แตกต่างจากที่กำหนดไว้ เพื่อให้สามารถเปลี่ยนแปลงค่าได้คุณจะต้องเพิ่มคำหลัก [ref]
ประเภทการอ้างอิงถูกส่งผ่านโดยการอ้างอิงและสามารถกลายพันธุ์ได้
แน่นอนว่าสตริงประเภทการอ้างอิงที่ไม่เปลี่ยนแปลงไม่แน่นอนนั้นเป็นสตริงที่ได้รับความนิยมมากที่สุด
รูปแบบอาร์เรย์ / การกำหนดค่าเริ่มต้น: ประเภทค่า -> หน่วยความจำศูนย์ [ชื่อ, ไปรษณีย์] [ชื่อ, ไปรษณีย์] ประเภทอ้างอิง -> หน่วยความจำศูนย์ -> null [อ้างอิง] [อ้างอิง]
class
หรือstruct
ประกาศเป็นเหมือนพิมพ์เขียวที่ใช้ในการสร้างอินสแตนซ์หรือวัตถุที่เวลาทำงาน หากคุณกำหนดclass
หรือstruct
เรียกว่าบุคคลบุคคลคือชื่อของประเภท หากคุณประกาศและกำหนดค่าเริ่มต้นตัวแปร p ชนิดบุคคล p จะถูกกล่าวถึงว่าเป็นวัตถุหรืออินสแตนซ์ของบุคคล หลายกรณีของคนประเภทเดียวที่สามารถสร้างและแต่ละกรณีจะมีค่าแตกต่างกันในมันและproperties
fields
A class
เป็นประเภทอ้างอิง เมื่อวัตถุของclass
ถูกสร้างขึ้นตัวแปรที่วัตถุได้รับมอบหมายถือการอ้างอิงไปยังหน่วยความจำนั้นเท่านั้น เมื่อการอ้างอิงวัตถุถูกกำหนดให้กับตัวแปรใหม่ตัวแปรใหม่หมายถึงวัตถุต้นฉบับ การเปลี่ยนแปลงที่ทำผ่านตัวแปรตัวเดียวจะมีผลกับตัวแปรตัวอื่น ๆ เพราะทั้งคู่อ้างถึงข้อมูลเดียวกัน
A struct
คือชนิดของค่า เมื่อมีการstruct
สร้างตัวแปรที่struct
ได้รับมอบหมายเก็บข้อมูลที่แท้จริงของ struct เมื่อstruct
กำหนดให้กับตัวแปรใหม่มันจะถูกคัดลอก ดังนั้นตัวแปรใหม่และตัวแปรดั้งเดิมจึงมีข้อมูลที่เหมือนกันสองชุดแยกกัน การเปลี่ยนแปลงที่ทำกับสำเนาหนึ่งชุดจะไม่มีผลกับสำเนาอื่น ๆ
โดยทั่วไปclasses
จะใช้เพื่อจำลองพฤติกรรมที่ซับซ้อนมากขึ้นหรือข้อมูลที่มีวัตถุประสงค์เพื่อแก้ไขหลังจากสร้างclass
วัตถุ Structs
เหมาะที่สุดสำหรับโครงสร้างข้อมูลขนาดเล็กที่มีข้อมูลเป็นหลักซึ่งไม่ได้ตั้งใจจะแก้ไขหลังจากที่struct
ถูกสร้างขึ้น
โครงสร้างที่ถือว่าเป็นประเภทของมูลค่าจะได้รับการจัดสรรบนสแต็กในขณะที่ออบเจ็กต์จะได้รับการจัดสรรบนฮีปในขณะที่การอ้างอิงออบเจ็กต์ (ตัวชี้) จะได้รับการจัดสรรบนสแต็ก
โครงสร้างได้รับการจัดสรรให้กับกอง นี่คือคำอธิบายที่เป็นประโยชน์:
นอกจากนี้คลาสเมื่ออินสแตนซ์ภายใน. NET จัดสรรหน่วยความจำบนฮีปหรือพื้นที่หน่วยความจำสำรองของ. NET ในขณะที่ structs ให้ประสิทธิภาพมากขึ้นเมื่ออินสแตนซ์เนื่องจากการจัดสรรในกอง นอกจากนี้ควรสังเกตว่าการส่งพารามิเตอร์ภายใน structs ทำได้โดยค่า