การแปลงอาเรย์แบบแปรผันร่วมจาก x เป็น y อาจทำให้เกิดข้อยกเว้นขณะทำงาน


142

ฉันมีprivate readonlyรายการLinkLabels ( IList<LinkLabel>) ในภายหลังฉันเพิ่มLinkLabels ในรายการนี้และเพิ่มป้ายกำกับเหล่านั้นลงในรายการFlowLayoutPanelต่อไปนี้:

foreach(var s in strings)
{
    _list.Add(new LinkLabel{Text=s});
}

flPanel.Controls.AddRange(_list.ToArray());

แสดงให้เห็นว่า Resharper Co-variant array conversion from LinkLabel[] to Control[] can cause run-time exception on write operationฉันเตือน:

โปรดช่วยฉันคิดออก:

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

คำตอบ:


154

สิ่งนี้หมายความว่าอะไร

Control[] controls = new LinkLabel[10]; // compile time legal
controls[0] = new TextBox(); // compile time legal, runtime exception

และในแง่ทั่วไปมากขึ้น

string[] array = new string[10];
object[] objs = array; // legal at compile time
objs[0] = new Foo(); // again legal, with runtime exception

ใน C # คุณได้รับอนุญาตให้อ้างอิงอาร์เรย์ของวัตถุ (ในกรณีของคุณ LinkLabels) เป็นอาร์เรย์ของประเภทฐาน (ในกรณีนี้เป็นอาร์เรย์ของการควบคุม) นอกจากนี้ยังรวบรวมเวลาตามกฎหมายในการกำหนดวัตถุอื่นที่เป็นControlของอาร์เรย์ ปัญหาคือว่าอาเรย์ไม่ได้เป็นอาเรย์ของการควบคุม ณ รันไทม์มันยังคงเป็นอาร์เรย์ของ LinkLabels เช่นนี้การมอบหมายหรือเขียนจะโยนข้อยกเว้น


ฉันเข้าใจความแตกต่างของเวลารันไทม์ / คอมไพล์เช่นเดียวกับในตัวอย่างของคุณ แต่ไม่ได้รับการแปลงจากประเภทพิเศษเป็นประเภทฐานถูกกฎหมาย? ยิ่งไปกว่านั้นฉันมีรายการที่พิมพ์แล้วและฉันเปลี่ยนจากLinkLabel(ประเภทเฉพาะ) เป็นControl(ประเภทฐาน)
TheVillageIdiot

2
ใช่การแปลงจาก LinkLabel เป็นตัวควบคุมนั้นถูกกฎหมาย แต่นั่นไม่เหมือนกับสิ่งที่เกิดขึ้นที่นี่ นี่คือการเตือนเกี่ยวกับการแปลงจากLinkLabel[]ไปControl[]ซึ่งยังคงเป็นกฎหมาย แต่สามารถมีปัญหารันไทม์ สิ่งที่เปลี่ยนไปคือวิธีการอ้างอิงอาเรย์ อาร์เรย์จะไม่เปลี่ยนแปลง เห็นปัญหาหรือไม่ อาร์เรย์ยังคงเป็นอาร์เรย์ของชนิดที่ได้รับ การอ้างอิงคือผ่านอาร์เรย์ของประเภทฐาน ดังนั้นจึงเป็นเวลารวบรวมทางกฎหมายในการกำหนดองค์ประกอบให้กับประเภทฐาน กระนั้นประเภทรันไทม์จะไม่รองรับ
Anthony Pegram

ในกรณีของคุณฉันไม่คิดว่าเป็นปัญหาคุณเพียงแค่ใช้อาร์เรย์เพื่อเพิ่มลงในรายการตัวควบคุม
Anthony Pegram

6
หากใครสงสัยว่าทำไมอาร์เรย์มีความแปรปรวนร่วมผิดใน C # นี่คือคำอธิบายของ Eric Lippert : มันถูกเพิ่มไปยัง CLR เพราะ Java ต้องการมันและผู้ออกแบบ CLR ต้องการที่จะสนับสนุนภาษาที่เหมือน Java เราเพิ่มและเพิ่มลงใน C # เพราะอยู่ใน CLR การตัดสินใจครั้งนี้ค่อนข้างเป็นที่ถกเถียงกันอยู่ในตอนนั้นและฉันก็ไม่ค่อยมีความสุขกับมัน แต่ตอนนี้เราไม่สามารถทำอะไรได้เลย
franssu

14

ฉันจะพยายามชี้แจงคำตอบของแอนโทนี่เพกราม

ประเภททั่วไปเป็น covariant อาร์กิวเมนต์ชนิดบางอย่างเมื่อมันส่งกลับค่าจากประเภทกล่าว (เช่นFunc<out TResult>กรณีของผลตอบแทนTResult, IEnumerable<out T>กรณีผลตอบแทนT) นั่นคือถ้ามีบางสิ่งที่ส่งคืนอินสแตนซ์ของTDerivedคุณสามารถทำงานกับอินสแตนซ์นั้นได้เช่นTBaseกัน

ประเภทสามัญมีความขัดแย้งในบางประเภทอาร์กิวเมนต์เมื่อมันยอมรับค่าของประเภทดังกล่าว (เช่นAction<in TArgument>ยอมรับกรณีของTArgument) นั่นคือถ้าสิ่งที่ต้องการกรณีของคุณสามารถเป็นอย่างดีผ่านในกรณีของTBaseTDerived

ดูเหมือนว่ามีเหตุผลที่ประเภททั่วไปซึ่งทั้งสองยอมรับและส่งคืนอินสแตนซ์ของบางประเภท (ยกเว้นว่ามีการกำหนดสองครั้งในลายเซ็นประเภททั่วไปเช่นCoolList<TIn, TOut>) ไม่ใช่ covariant หรือ contravariant บนอาร์กิวเมนต์ประเภทที่สอดคล้องกัน ยกตัวอย่างเช่นListที่กำหนดไว้ใน .NET 4 List<T>ไม่ได้หรือList<in T>List<out T>

เหตุผลความเข้ากันได้บางอย่างอาจทำให้ Microsoft เพิกเฉยต่ออาร์กิวเมนต์นั้นและสร้างอาร์เรย์ covariant บนอาร์กิวเมนต์ชนิดค่าของพวกเขา บางทีพวกเขาทำการวิเคราะห์และพบว่าคนส่วนใหญ่จะใช้อาร์เรย์ราวกับว่าพวกเขาอ่านได้อย่างเดียว (นั่นคือพวกเขาใช้เพียงตัวเริ่มต้นของอาเรย์ในการเขียนข้อมูลลงในอาเรย์) และดังนั้นข้อดีของข้อผิดพลาด ข้อผิดพลาดเมื่อมีคนพยายามใช้ความแปรปรวนร่วมเมื่อเขียนลงในอาร์เรย์ ดังนั้นจึงอนุญาต แต่ไม่ได้รับการสนับสนุน

สำหรับคำถามเดิมของคุณlist.ToArray()สร้างใหม่LinkLabel[]ที่มีค่าคัดลอกมาจากรายการเดิมและการกำจัดของการเตือน (ที่เหมาะสม) คุณจะต้องผ่านไปControl[] จะทำงาน: ยอมรับว่าเป็นข้อโต้แย้งและผลตอบแทน; ใช้แบบอ่านอย่างเดียวซึ่งต้องขอบคุณความแปรปรวนร่วมซึ่งสามารถส่งผ่านไปยังวิธีการที่ยอมรับว่าเป็นอาร์กิวเมนต์AddRangelist.ToArray<Control>()ToArray<TSource>IEnumerable<TSource>TSource[]List<LinkLabel>IEnumerable<out LinkLabel>IEnumerableIEnumerable<Control>


11

"ทางออก" ที่ตรงไปตรงมาที่สุด

flPanel.Controls.AddRange(_list.AsEnumerable());

ขณะนี้เนื่องจากคุณกำลังเปลี่ยนแปลงList<LinkLabel>ไปอย่างIEnumerable<Control>ไม่มีเงื่อนไขอีกต่อไปเนื่องจากคุณไม่สามารถ "เพิ่ม" รายการลงในรายการที่นับได้


10

คำเตือนเกิดจากความจริงที่ว่าคุณสามารถเพิ่มทฤษฎีControlอื่นที่ไม่ใช่ a LinkLabelถึงถึงLinkLabel[]ถึงการControl[]อ้างอิงถึงมัน สิ่งนี้จะทำให้เกิดข้อยกเว้นรันไทม์

แปลงที่เกิดขึ้นที่นี่เพราะต้องใช้เวลาAddRangeControl[]

โดยทั่วไปแล้วการแปลงคอนเทนเนอร์ประเภทที่ได้รับมาเป็นคอนเทนเนอร์ประเภทฐานจะปลอดภัยเฉพาะในกรณีที่คุณไม่สามารถแก้ไขคอนเทนเนอร์ตามวิธีที่ระบุไว้ในภายหลัง อาร์เรย์ไม่ตอบสนองความต้องการนั้น


5

สาเหตุที่แท้จริงของปัญหาอธิบายไว้อย่างถูกต้องในคำตอบอื่น ๆ แต่เพื่อแก้ไขคำเตือนคุณสามารถเขียนได้เสมอ:

_list.ForEach(lnkLbl => flPanel.Controls.Add(lnkLbl));

2

ด้วย VS 2008 ฉันไม่ได้รับคำเตือนนี้ สิ่งนี้จะต้องใหม่กับ. NET 4.0
ชี้แจง: ตาม Sam Mackrill เป็น Resharper ที่แสดงคำเตือน

คอมไพเลอร์ C # ไม่ทราบว่าAddRangeจะไม่แก้ไขอาร์เรย์ที่ส่งผ่านไป เนื่องจากAddRangeมีพารามิเตอร์ชนิดControl[]ในทางทฤษฎีแล้วลองกำหนด a TextBoxให้กับอาเรย์ซึ่งจะถูกต้องสมบูรณ์สำหรับอาเรย์ที่แท้จริงControlแต่อาเรย์นั้นในความเป็นจริงแล้วอาเรย์ของLinkLabelsและจะไม่ยอมรับการกำหนดเช่นนั้น

การสร้างอาร์เรย์ร่วมใน c # เป็นการตัดสินใจที่ผิดพลาดของ Microsoft ในขณะที่มันอาจเป็นความคิดที่ดีที่จะสามารถกำหนดอาเรย์ของประเภทที่ได้รับมาให้อาเรย์ของประเภทฐานในสถานที่แรกนี้อาจนำไปสู่ข้อผิดพลาด runtime!


2
ฉันได้รับคำเตือนนี้จาก Resharper
Sam Mackrill

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