ทำไมความแปรปรวนร่วมและความแปรปรวนไม่สนับสนุนประเภทค่า


149

IEnumerable<T>เป็นตัวแปรร่วมแต่ไม่สนับสนุนประเภทค่าเพียงแค่ประเภทอ้างอิงเท่านั้น คอมไพล์รหัสด้านล่างเรียบง่ายสำเร็จแล้ว:

IEnumerable<string> strList = new List<string>();
IEnumerable<object> objList = strList;

แต่การเปลี่ยนจากstringเป็นintจะได้รับการรวบรวมข้อผิดพลาด:

IEnumerable<int> intList = new List<int>();
IEnumerable<object> objList = intList;

เหตุผลอธิบายไว้ในMSDN :

ความแปรปรวนใช้กับประเภทอ้างอิงเท่านั้น หากคุณระบุประเภทค่าสำหรับพารามิเตอร์ประเภทตัวแปรพารามิเตอร์ประเภทนั้นจะไม่เปลี่ยนแปลงสำหรับชนิดที่สร้างขึ้นที่เกิดขึ้น

ฉันค้นหาและพบว่าบางคำถามที่กล่าวถึงเหตุผลคือการชกมวยระหว่างประเภทค่าและประเภทการอ้างอิงมวยระหว่างชนิดของมูลค่าและประเภทของการอ้างอิงแต่มันยังไม่ชัดเจนในใจฉันมากนักว่าทำไมการชกมวยถึงเป็นเหตุผล?

ใครช่วยกรุณาให้คำอธิบายที่เรียบง่ายและมีรายละเอียดว่าทำไมความแปรปรวนร่วมและความแตกต่างไม่สนับสนุนประเภทของค่าและการชกมวยมีผลต่อเรื่องนี้อย่างไร?


3
ดูคำตอบของเอริคกับคำถามที่คล้ายกันฉัน: stackoverflow.com/questions/4096299/...
ธ อร์น

1
เป็นไปได้ที่ซ้ำกันของcant-convert-value-type-array-to-params-object
nawfal

คำตอบ:


126

โดยทั่วไปความแปรปรวนจะมีผลเมื่อ CLR สามารถมั่นใจได้ว่ามันไม่จำเป็นต้องทำการเปลี่ยนแปลงใด ๆกับค่า การอ้างอิงทั้งหมดมีลักษณะเหมือนกัน - ดังนั้นคุณสามารถใช้การIEnumerable<string>เป็นแบบ a IEnumerable<object>โดยไม่มีการเปลี่ยนแปลงใด ๆ ในการแสดง รหัสเนทีฟนั้นไม่จำเป็นต้องรู้ว่าคุณกำลังทำอะไรกับค่าทั้งหมดตราบใดที่โครงสร้างพื้นฐานรับประกันว่ามันจะถูกต้องแน่นอน

สำหรับประเภทค่าที่ไม่ได้ผล - การใช้IEnumerable<int>เป็นแบบIEnumerable<object>รหัสที่ใช้ลำดับจะต้องทราบว่าจะทำการแปลงมวยหรือไม่

คุณอาจต้องการอ่านโพสต์บล็อกของ Eric Lippert เกี่ยวกับการเป็นตัวแทนและตัวตนสำหรับข้อมูลเพิ่มเติมเกี่ยวกับหัวข้อนี้โดยทั่วไป

แก้ไข: มีบล็อกของเอริคอ่านโพสต์ตัวเองอย่างน้อยก็มากเกี่ยวกับตัวตนเป็นตัวแทนแม้ว่าทั้งสองมีการเชื่อมโยง โดยเฉพาะอย่างยิ่ง:

นี่คือเหตุผลที่การแปลง covariant และ contravariant ของประเภทอินเตอร์เฟสและผู้ได้รับมอบสิทธิ์นั้นต้องการให้อาร์กิวเมนต์ประเภทที่แตกต่างกันทั้งหมดเป็นประเภทอ้างอิง เพื่อให้แน่ใจว่าการแปลงการอ้างอิงชุดตัวเลือกจะคงการสงวนตัวตนไว้เสมอการแปลงทั้งหมดที่เกี่ยวข้องกับประเภทอาร์กิวเมนต์จะต้องได้รับการสงวนไว้ด้วย วิธีที่ง่ายที่สุดในการตรวจสอบให้แน่ใจว่าการแปลงที่ไม่สำคัญในการโต้แย้งประเภทเป็นการรักษาตัวตนคือการ จำกัด ให้เป็นการแปลงอ้างอิง


5
@CuongLe: มันเป็นรายละเอียดการใช้งานในบางความรู้สึก แต่มันเป็นเหตุผลพื้นฐานของการ จำกัด ผมเชื่อว่า
Jon Skeet

2
@ AndréCaron: โพสต์บล็อกของ Eric เป็นสิ่งสำคัญที่นี่ - มันไม่ได้เป็นเพียงตัวแทน แต่ยังเก็บรักษาตัวตน แต่การอนุรักษ์การเป็นตัวแทนหมายถึงรหัสที่สร้างขึ้นไม่จำเป็นต้องสนใจสิ่งนี้เลย
Jon Skeet

1
แม่นยำตัวตนไม่สามารถเก็บรักษาไว้เพราะไม่ได้เป็นชนิดย่อยของint objectความจริงที่ว่าจำเป็นต้องมีการเปลี่ยนแปลงเชิงดำเนินการเป็นผลมาจากสิ่งนี้
André Caron

3
int ไม่ใช่วัตถุย่อยของวัตถุอย่างไร? Int32 สืบทอดมาจาก System.ValueType ซึ่งสืบทอดมาจาก System.Object
David Klempfner

1
@DavidKlempfner ฉันคิดว่าความคิดเห็น @ AndréCaronเป็นประโยคที่ไม่ดี ชนิดของค่าใด ๆ เช่นInt32มีรูปแบบการดำเนินการสองรูปแบบคือ "ชนิดบรรจุกล่อง" และ "ไม่มีกล่อง" คอมไพเลอร์ต้องแทรกรหัสเพื่อแปลงจากแบบฟอร์มหนึ่งเป็นอีกแบบหนึ่งแม้ว่าปกติจะมองไม่เห็นที่ระดับซอร์สโค้ด ผลเพียง "ชนิดบรรจุกล่อง" รูปแบบคือการพิจารณาโดยระบบพื้นฐานที่จะเป็นชนิดย่อยของแต่เรียบเรียงเกี่ยวข้องโดยอัตโนมัติกับเรื่องนี้เมื่อใดก็ตามที่ประเภทค่าถูกกำหนดให้กับอินเตอร์เฟซที่รองรับหรือบางสิ่งบางอย่างจากประเภทobject object
Steve

10

มันอาจจะง่ายกว่าที่จะเข้าใจถ้าคุณคิดเกี่ยวกับการเป็นตัวแทนพื้นฐาน (แม้ว่านี่จะเป็นรายละเอียดการนำไปปฏิบัติจริง ๆ ) นี่คือชุดของสตริง:

IEnumerable<string> strings = new[] { "A", "B", "C" };

คุณสามารถคิดว่าstringsมีตัวแทนดังต่อไปนี้:

[0]: การอ้างอิงสตริง -> "A"
[1]: การอ้างอิงสตริง -> "B"
[2]: การอ้างอิงสตริง -> "C"

มันเป็นคอลเลกชันของสามองค์ประกอบแต่ละการอ้างอิงถึงสตริง คุณสามารถส่งสิ่งนี้ไปยังชุดของวัตถุ:

IEnumerable<object> objects = (IEnumerable<object>) strings;

โดยพื้นฐานแล้วมันเป็นตัวแทนเดียวกันยกเว้นตอนนี้การอ้างอิงเป็นการอ้างอิงวัตถุ:

[0]: การอ้างอิงวัตถุ -> "A"
[1]: การอ้างอิงวัตถุ -> "B"
[2]: การอ้างอิงวัตถุ -> "C"

การเป็นตัวแทนเหมือนกัน การอ้างอิงจะถือว่าแตกต่างกัน คุณไม่สามารถเข้าถึงstring.Lengthคุณสมบัติได้อีกต่อไปแต่คุณยังสามารถโทรobject.GetHashCode()ได้ เปรียบเทียบสิ่งนี้กับชุดของ ints:

IEnumerable<int> ints = new[] { 1, 2, 3 };
[0]: int = 1
[1]: int = 2
[2]: int = 3

ในการแปลงเป็นIEnumerable<object>ข้อมูลจะต้องทำการแปลงโดย Boxing the ints:

[0]: การอ้างอิงวัตถุ -> 1
[1]: การอ้างอิงวัตถุ -> 2
[2]: การอ้างอิงวัตถุ -> 3

การแปลงนี้ต้องการมากกว่าการร่าย


2
มวยไม่ใช่แค่ "รายละเอียดการใช้งาน" ชนิดของค่าที่บรรจุอยู่จะถูกเก็บไว้ในลักษณะเดียวกับคลาสของวัตถุและมีพฤติกรรมเท่าที่โลกภายนอกสามารถบอกได้เช่นเดียวกับวัตถุในชั้นเรียน ความแตกต่างเพียงอย่างเดียวคือภายในคำจำกัดความของชนิดค่าแบบกล่องthisหมายถึงโครงสร้างที่เขตข้อมูลซ้อนทับวัตถุฮีปที่เก็บมันไว้แทนที่จะอ้างถึงวัตถุที่เก็บมันไว้ ไม่มีวิธีใหม่ทั้งหมดสำหรับอินสแตนซ์ชนิดค่าชนิดบรรจุกล่องเพื่อรับการอ้างอิงไปยังวัตถุฮีปที่ล้อมรอบ
supercat

7

ฉันคิดว่าทุกอย่างเริ่มต้นจากคำจำกัดความของLSP(หลักการทดแทน Liskov) ซึ่ง climes:

ถ้า q (x) เป็นคุณสมบัติที่พิสูจน์ได้เกี่ยวกับวัตถุ x ของประเภท T ดังนั้น q (y) ควรเป็นจริงสำหรับวัตถุ y ของประเภท S โดยที่ S เป็นชนิดย่อยของ T

แต่ประเภทค่าตัวอย่างเช่นintไม่สามารถแทนในobject C#พิสูจน์ง่ายมาก:

int myInt = new int();
object obj1 = myInt ;
object obj2 = myInt ;
return ReferenceEquals(obj1, obj2);

สิ่งนี้จะส่งคืนfalseแม้ว่าเราจะกำหนด"อ้างอิง" เดียวกันให้กับวัตถุ


1
ฉันคิดว่าคุณกำลังใช้หลักการที่ถูกต้อง แต่ไม่มีข้อพิสูจน์ที่จะทำ: intไม่ใช่ประเภทย่อยobjectดังนั้นหลักการจึงไม่สามารถนำไปใช้ได้ "การพิสูจน์" ของคุณอาศัยการเป็นตัวแทนระดับกลางIntegerซึ่งเป็นประเภทย่อยobjectและภาษาที่มีการแปลงโดยนัย ( object obj1=myInt;เป็นจริงขยายไปถึงobject obj1=new Integer(myInt);)
André Caron

ภาษาจะดูแลการแคสต์ที่ถูกต้องระหว่างประเภท แต่พฤติกรรม ints ไม่สอดคล้องกับสิ่งที่เราคาดหวังจากประเภทย่อยของวัตถุ
Tigran

จุดรวมของฉันคืออย่างแม่นยำว่าไม่ได้เป็นชนิดย่อยของint objectนอกจากนี้ LSP ใช้ไม่ได้เพราะmyInt, obj1และobj2อ้างถึงสามวัตถุที่แตกต่างกันอย่างใดอย่างหนึ่งintและสอง (ซ่อน) Integers
André Caron

22
@ André: C # ไม่ใช่ Java intคำหลักของ C # เป็นชื่อแทนของ BCL System.Int32ซึ่งอันที่จริงแล้วเป็นประเภทย่อยของobject(นามแฝงSystem.Object) ในความเป็นจริงintของชั้นฐานเป็นผู้ที่ชั้นฐานคือSystem.ValueType ลองประเมินนิพจน์ต่อไปนี้และดู:System.Object typeof(int).BaseType.BaseTypeเหตุผลที่ReferenceEqualsส่งกลับค่าเท็จนี่คือintกล่องถูกแบ่งออกเป็นสองกล่องแยกกันและข้อมูลประจำตัวของแต่ละกล่องนั้นแตกต่างกันสำหรับกล่องอื่น ๆ ดังนั้นการดำเนินการชกมวยสองรายการจะให้ผลลัพธ์สองวัตถุที่ไม่เหมือนกันเสมอโดยไม่คำนึงถึงค่าที่บรรจุไว้
Allon Guralnek

@AllonGuralnek: แต่ละประเภทค่า (เช่นSystem.Int32หรือList<String>.Enumerator) หมายถึงสองชนิดของสิ่งต่าง ๆ : ประเภทของที่เก็บข้อมูลและประเภทของวัตถุฮีป (บางครั้งเรียกว่า ที่เก็บสินค้าที่ได้รับมาจากประเภทSystem.ValueTypeจะเก็บอดีต; วัตถุกองที่มีประเภททำเช่นเดียวกันจะถือหลัง ในภาษาส่วนใหญ่จะมีตัวละครขนาดใหญ่ขึ้นมาจากอดีตอย่างหลังและแบบแคบ ๆ จากหลังไปสู่อดีต โปรดทราบว่าในขณะที่ประเภทค่าชนิดบรรจุกล่องมีตัวบอกประเภทเดียวกันกับที่เก็บสินค้าประเภทค่า ...
supercat

3

รายละเอียดของการติดตั้งใช้งานได้ลงมา: ชนิดของค่าจะถูกนำไปใช้แตกต่างกันไปตามประเภทการอ้างอิง

หากคุณบังคับประเภทค่าที่จะถือว่าเป็นประเภทการอ้างอิง (เช่นกล่องพวกเขาเช่นโดยการอ้างอิงพวกเขาผ่านทางอินเตอร์เฟซ) คุณจะได้รับความแปรปรวน

วิธีที่ง่ายที่สุดในการมองเห็นความแตกต่างก็คือการพิจารณาArray: อาเรย์ของประเภทค่าจะถูกนำมารวมกันในหน่วยความจำต่อเนื่อง (โดยตรง) ซึ่งในฐานะที่เป็นอาเรย์ของประเภทการอ้างอิงมีเพียงการอ้างอิง วัตถุที่ถูกชี้ไปจะถูกจัดสรรแยกต่างหาก

ปัญหา (ที่เกี่ยวข้อง) อื่น ๆ (*) คือ (เกือบ) ประเภทการอ้างอิงทั้งหมดมีการแทนค่าเดียวกันสำหรับวัตถุประสงค์ความแปรปรวนและรหัสจำนวนมากไม่จำเป็นต้องทราบถึงความแตกต่างระหว่างประเภทดังนั้นจึงมีความเป็นไปได้ในการร่วมและตรงกันข้าม ถูกใช้งาน - บ่อยครั้งที่ละเลยการตรวจสอบประเภทพิเศษ)

(*) อาจเป็นปัญหาเดียวกัน ...

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.