ฉันรู้ว่าโครงสร้างใน. NET ไม่รองรับการสืบทอด แต่ไม่ชัดเจนว่าทำไมจึงถูก จำกัด ด้วยวิธีนี้
เหตุผลทางเทคนิคใดที่ป้องกันไม่ให้โครงสร้างรับช่วงจากโครงสร้างอื่น ๆ
ฉันรู้ว่าโครงสร้างใน. NET ไม่รองรับการสืบทอด แต่ไม่ชัดเจนว่าทำไมจึงถูก จำกัด ด้วยวิธีนี้
เหตุผลทางเทคนิคใดที่ป้องกันไม่ให้โครงสร้างรับช่วงจากโครงสร้างอื่น ๆ
คำตอบ:
ประเภทค่าเหตุผลไม่สามารถรองรับการสืบทอดได้เนื่องจากอาร์เรย์
ปัญหาคือด้วยเหตุผลด้านประสิทธิภาพและ GC อาร์เรย์ของประเภทค่าจะถูกเก็บไว้ "แบบอินไลน์" ตัวอย่างเช่นnew FooType[10] {...}
หากFooType
เป็นชนิดการอ้างอิงวัตถุ 11 ชิ้นจะถูกสร้างขึ้นบนฮีปที่มีการจัดการ (หนึ่งรายการสำหรับอาร์เรย์และ 10 สำหรับอินสแตนซ์แต่ละประเภท) ถ้าFooType
เป็นประเภทค่าแทนจะมีเพียงอินสแตนซ์เดียวเท่านั้นที่ถูกสร้างขึ้นบนฮีปที่มีการจัดการ - สำหรับอาร์เรย์เอง (เนื่องจากค่าอาร์เรย์แต่ละค่าจะถูกจัดเก็บ "แบบอินไลน์" ด้วยอาร์เรย์)
ตอนนี้สมมติว่าเรามีมรดกที่มีประเภทมูลค่า เมื่อรวมกับพฤติกรรมดังกล่าว "การจัดเก็บข้อมูลแบบอินไลน์" ของอาร์เรย์สิ่งที่ไม่ดีเกิดขึ้นสามารถมองเห็นได้ใน C
พิจารณารหัสหลอก C # นี้:
struct Base
{
public int A;
}
struct Derived : Base
{
public int B;
}
void Square(Base[] values)
{
for (int i = 0; i < values.Length; ++i)
values [i].A *= 2;
}
Derived[] v = new Derived[2];
Square (v);
ตามกฎการแปลงปกติ a Derived[]
สามารถแปลงเป็น a Base[]
(ดีขึ้นหรือแย่ลง) ดังนั้นหากคุณ s / struct / class / g สำหรับตัวอย่างข้างต้นมันจะคอมไพล์และทำงานตามที่คาดไว้โดยไม่มีปัญหา แต่ถ้าBase
และDerived
เป็นชนิดค่าและอาร์เรย์เก็บค่าไว้ในบรรทัดแสดงว่าเรามีปัญหา
เรามีปัญหาเพราะSquare()
ไม่รู้อะไรเลยDerived
มันจะใช้เฉพาะเลขคณิตตัวชี้เพื่อเข้าถึงแต่ละองค์ประกอบของอาร์เรย์โดยเพิ่มขึ้นด้วยจำนวนคงที่ ( sizeof(A)
) การชุมนุมจะคลุมเครือเช่น:
for (int i = 0; i < values.Length; ++i)
{
A* value = (A*) (((char*) values) + i * sizeof(A));
value->A *= 2;
}
(ใช่นั่นคือแอสเซมบลีที่น่ารังเกียจ แต่ประเด็นก็คือเราจะเพิ่มขึ้นผ่านอาร์เรย์ที่ค่าคงที่เวลาคอมไพล์ที่ทราบโดยไม่ทราบว่ามีการใช้ประเภทที่ได้รับมา)
ดังนั้นหากสิ่งนี้เกิดขึ้นจริงเราจะมีปัญหาหน่วยความจำเสียหาย โดยเฉพาะภายในSquare()
, values[1].A*=2
จะจริงจะแก้ไขvalues[0].B
!
พยายามแก้ปัญหาว่า !
ลองนึกภาพโครงสร้างที่รองรับการถ่ายทอดทางพันธุกรรม จากนั้นประกาศ:
BaseStruct a;
InheritedStruct b; //inherits from BaseStruct, added fields, etc.
a = b; //?? expand size during assignment?
จะหมายถึงตัวแปร struct ไม่มีขนาดคงที่และนั่นคือเหตุผลที่เรามีประเภทอ้างอิง
ยิ่งไปกว่านั้นให้พิจารณาสิ่งนี้:
BaseStruct[] baseArray = new BaseStruct[1000];
baseArray[500] = new InheritedStruct(); //?? morph/resize the array?
Foo
สืบทอดโครงสร้างBar
ไม่ควรอนุญาตให้Foo
กำหนด a ให้กับ a Bar
แต่การประกาศโครงสร้างด้วยวิธีนี้อาจทำให้เกิดผลประโยชน์สองสามประการ: (1) สร้างสมาชิกที่มีชื่อพิเศษBar
เป็นรายการแรกในFoo
และFoo
รวมไว้ด้วย ชื่อของสมาชิกที่นามแฝงให้กับสมาชิกผู้ที่อยู่ในBar
การอนุญาตให้รหัสที่ได้นำมาใช้Bar
เพื่อนำมาปรับใช้Foo
แทนโดยไม่ต้องเปลี่ยนอ้างอิงทั้งหมดไปthing.BarMember
ด้วย thing.theBar.BarMember
และยังคงรักษาความสามารถในการอ่านและเขียนทั้งหมดBar
ของเขตข้อมูลเป็นกลุ่ม; ...
โครงสร้างไม่ใช้การอ้างอิง (เว้นแต่จะมีการใส่กล่อง แต่คุณควรพยายามหลีกเลี่ยงสิ่งนั้น) ดังนั้นความหลากหลายจึงไม่มีความหมายเนื่องจากไม่มีการกำหนดทิศทางผ่านตัวชี้อ้างอิง โดยปกติออบเจ็กต์จะอาศัยอยู่บนฮีปและถูกอ้างอิงผ่านตัวชี้อ้างอิง แต่โครงสร้างจะถูกจัดสรรบนสแต็ก (เว้นแต่จะเป็นแบบบรรจุกล่อง) หรือได้รับการจัดสรร "ภายใน" หน่วยความจำที่ครอบครองโดยชนิดการอ้างอิงบนฮีป
Foo
ที่มีเขตข้อมูลประเภทโครงสร้างที่Bar
สามารถถือว่าBar
สมาชิกเป็นของตัวเองได้ดังนั้นPoint3d
ระดับทำได้เช่นแค็ปซูลPoint2d xy
แต่หมายถึงX
ของเขตข้อมูลที่เป็นทั้งหรือxy.X
X
นี่คือสิ่งที่เอกสารกล่าว:
โครงสร้างมีประโยชน์อย่างยิ่งสำหรับโครงสร้างข้อมูลขนาดเล็กที่มีความหมายเชิงคุณค่า จำนวนเชิงซ้อนจุดในระบบพิกัดหรือคู่คีย์ - ค่าในพจนานุกรมล้วนเป็นตัวอย่างที่ดีของโครงสร้าง สิ่งสำคัญสำหรับโครงสร้างข้อมูลเหล่านี้คือมีสมาชิกข้อมูลน้อยซึ่งไม่จำเป็นต้องใช้การสืบทอดหรือข้อมูลประจำตัวอ้างอิงและสามารถนำไปใช้งานได้อย่างสะดวกโดยใช้ความหมายของค่าโดยที่การมอบหมายจะคัดลอกค่าแทนการอ้างอิง
โดยพื้นฐานแล้วพวกเขาควรจะเก็บข้อมูลธรรมดาดังนั้นจึงไม่มี "คุณสมบัติพิเศษ" เช่นการสืบทอด ในทางเทคนิคอาจเป็นไปได้ที่พวกเขาจะสนับสนุนการสืบทอดแบบ จำกัด (ไม่ใช่ความหลากหลายเนื่องจากอยู่ในสแต็ก) แต่ฉันเชื่อว่านี่เป็นทางเลือกในการออกแบบที่จะไม่รองรับการสืบทอด (เช่นเดียวกับสิ่งอื่น ๆ ใน. NET ภาษา)
ในทางกลับกันฉันเห็นด้วยกับผลประโยชน์ของมรดกและฉันคิดว่าเราทุกคนมาถึงจุดที่เราต้องการstruct
สืบทอดจากคนอื่นแล้วและตระหนักดีว่ามันเป็นไปไม่ได้ แต่เมื่อถึงจุดนั้นโครงสร้างข้อมูลอาจสูงมากจนควรเป็นคลาสอยู่ดี
Point3D
จาก a Point2D
คุณจะไม่สามารถ เพื่อใช้ a Point3D
แทนPoint2D
แต่คุณไม่จำเป็นต้องนำมาใช้ใหม่Point3D
ทั้งหมดตั้งแต่เริ่มต้น) นั่นคือวิธีที่ฉันตีความมันอย่างไรก็ตาม ...
class
มากกว่าstruct
เมื่อเหมาะสม
คลาสเช่นการสืบทอดเป็นไปไม่ได้เนื่องจากโครงสร้างถูกวางโดยตรงบนสแต็ก โครงสร้างที่สืบทอดจะใหญ่กว่านั้นก็คือพาเรนต์ แต่ JIT ไม่รู้และพยายามใส่พื้นที่น้อยเกินไปมากเกินไป ฟังดูไม่ชัดเจนเล็กน้อยลองเขียนตัวอย่าง:
struct A {
int property;
} // sizeof A == sizeof int
struct B : A {
int childproperty;
} // sizeof B == sizeof int * 2
หากเป็นไปได้จะมีข้อผิดพลาดกับตัวอย่างต่อไปนี้:
void DoSomething(A arg){};
...
B b;
DoSomething(b);
มีการจัดสรรพื้นที่สำหรับขนาด A ไม่ใช่สำหรับขนาด B
มีจุดที่อยากจะแก้ไข แม้ว่าเหตุผลที่โครงสร้างไม่สามารถสืบทอดได้เนื่องจากโครงสร้างเหล่านี้อาศัยอยู่บนสแต็กเป็นสิ่งที่ถูกต้อง แต่ก็เป็นคำอธิบายที่ถูกต้องเพียงครึ่งเดียว โครงสร้างเช่นเดียวกับประเภทค่าอื่น ๆสามารถอยู่ในสแต็กได้ เพราะมันจะขึ้นอยู่กับที่ตัวแปรมีการประกาศว่าพวกเขาทั้งสองจะอาศัยอยู่ในสแต็คหรือในกอง ซึ่งจะเกิดขึ้นเมื่อเป็นตัวแปรท้องถิ่นหรือฟิลด์อินสแตนซ์ตามลำดับ
ในการพูดนั้น Cecil มีชื่อที่ตอกไว้อย่างถูกต้อง
ฉันขอเน้นย้ำสิ่งนี้ประเภทของค่าสามารถอยู่ในสแต็กได้ นี่ไม่ได้หมายความว่าพวกเขาจะทำเช่นนั้นเสมอไป ตัวแปรท้องถิ่นรวมถึงพารามิเตอร์วิธีการจะ คนอื่น ๆ จะไม่ทำ อย่างไรก็ตามมันยังคงเป็นเหตุผลที่พวกเขาไม่สามารถสืบทอดได้ :-)
โครงสร้างถูกจัดสรรบนสแต็ก ซึ่งหมายความว่าค่าความหมายนั้นค่อนข้างฟรีและการเข้าถึงสมาชิกของโครงสร้างนั้นมีราคาถูกมาก สิ่งนี้ไม่ได้ป้องกันความหลากหลาย
คุณสามารถกำหนดให้โครงสร้างแต่ละส่วนเริ่มต้นด้วยตัวชี้ไปยังตารางฟังก์ชันเสมือนได้ นี่จะเป็นปัญหาด้านประสิทธิภาพ (โครงสร้างทุกอย่างจะมีขนาดเท่าตัวชี้เป็นอย่างน้อย) แต่ก็ทำได้ สิ่งนี้จะช่วยให้ฟังก์ชันเสมือนจริง
แล้วการเพิ่มช่องล่ะ?
เมื่อคุณจัดสรรโครงสร้างบนสแต็กคุณจะจัดสรรพื้นที่จำนวนหนึ่ง พื้นที่ที่ต้องการจะถูกกำหนดในเวลาคอมไพล์ (ไม่ว่าจะก่อนเวลาหรือเมื่อ JITting) หากคุณเพิ่มเขตข้อมูลแล้วกำหนดเป็นประเภทฐาน:
struct A
{
public int Integer1;
}
struct B : A
{
public int Integer2;
}
A a = new B();
สิ่งนี้จะเขียนทับส่วนที่ไม่รู้จักบางส่วนของสแต็ก
อีกทางเลือกหนึ่งคือสำหรับรันไทม์เพื่อป้องกันสิ่งนี้โดยเขียนเฉพาะ sizeof (A) ไบต์ลงในตัวแปร A
จะเกิดอะไรขึ้นถ้า B แทนที่เมธอดใน A และอ้างถึงฟิลด์ Integer2 รันไทม์จะพ่น MemberAccessException หรือเมธอดจะเข้าถึงข้อมูลแบบสุ่มบนสแต็กแทน สิ่งเหล่านี้ไม่ได้รับอนุญาต
มีความปลอดภัยอย่างสมบูรณ์ที่จะมีการสืบทอดโครงสร้างตราบใดที่คุณไม่ได้ใช้โครงสร้างหลายรูปแบบหรือตราบเท่าที่คุณไม่ได้เพิ่มฟิลด์เมื่อรับช่วง แต่สิ่งเหล่านี้ไม่มีประโยชน์มากนัก
นี่เป็นคำถามที่พบบ่อยมาก ฉันรู้สึกว่าการเพิ่มประเภทค่าจะถูกเก็บไว้ "ในตำแหน่ง" ที่คุณประกาศตัวแปร นอกเหนือจากรายละเอียดการนำไปใช้งานแล้วซึ่งหมายความว่าไม่มีส่วนหัวของออบเจ็กต์ที่ระบุบางสิ่งเกี่ยวกับออบเจ็กต์มีเพียงตัวแปรเท่านั้นที่รู้ว่ามีข้อมูลประเภทใดอยู่ที่นั่น
โครงสร้างรองรับอินเทอร์เฟซดังนั้นคุณสามารถทำสิ่งที่หลากหลายด้วยวิธีนั้น
IL เป็นภาษาที่ใช้สแต็กดังนั้นการเรียกเมธอดที่มีอาร์กิวเมนต์จะมีลักษณะดังนี้:
เมื่อเมธอดทำงานมันจะแสดงไบต์บางส่วนออกจากสแต็กเพื่อรับอาร์กิวเมนต์ มันรู้ว่าวิธีการหลายไบต์จะปรากฏออกเพราะอาร์กิวเมนต์เป็นทั้งตัวชี้ชนิดการอ้างอิง (เสมอ 4 ไบต์ 32 บิต) หรือจะเป็นชนิดของมูลค่าที่ขนาดเป็นที่รู้จักกันเสมอว่า
หากเป็นตัวชี้ประเภทการอ้างอิงเมธอดจะค้นหาวัตถุในฮีปและได้รับการจัดการประเภทซึ่งชี้ไปยังตารางวิธีการที่จัดการกับวิธีการเฉพาะสำหรับชนิดนั้น ๆ หากเป็นประเภทค่าก็ไม่จำเป็นต้องค้นหาตารางเมธอดเนื่องจากชนิดค่าไม่รองรับการสืบทอดดังนั้นจึงมีวิธีการ / ประเภทที่เป็นไปได้เพียงชุดเดียวเท่านั้น
หากประเภทค่ารองรับการสืบทอดจะมีค่าโสหุ้ยพิเศษที่ชนิดของโครงสร้างนั้นจะต้องวางไว้บนสแต็กเช่นเดียวกับค่าของมันซึ่งจะหมายถึงการค้นหาตารางวิธีการบางประเภทสำหรับอินสแตนซ์คอนกรีตเฉพาะของประเภท สิ่งนี้จะขจัดข้อได้เปรียบด้านความเร็วและประสิทธิภาพของประเภทมูลค่า