Konversi array co-varian dari x ke y dapat menyebabkan pengecualian run-time

142

Saya punya private readonlydaftar LinkLabels ( IList<LinkLabel>). Saya kemudian menambahkan LinkLabels ke daftar ini dan menambahkan label-label itu ke FlowLayoutPanelseperti berikut:

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

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

Menunjukkan Resharper saya peringatan: Co-variant array conversion from LinkLabel[] to Control[] can cause run-time exception on write operation.

Tolong bantu saya untuk mencari tahu:

  1. Apa artinya ini?
  2. Ini adalah kontrol pengguna dan tidak akan diakses oleh banyak objek untuk men-setup label, jadi menjaga kode seperti itu tidak akan mempengaruhinya.
TheVillageIdiot
sumber

Jawaban:

154

Artinya adalah ini

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

Dan secara umum

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

Dalam C #, Anda diizinkan untuk mereferensikan array objek (dalam kasus Anda, LinkLabels) sebagai array tipe dasar (dalam hal ini, sebagai array Kontrol). Ini juga waktu kompilasi legal untuk menetapkan objek lain yang merupakan Controlke array. Masalahnya adalah array sebenarnya bukan array Kontrol. Saat runtime, itu masih berupa array dari LinkLabels. Dengan demikian, tugas, atau menulis, akan memberikan pengecualian.

Anthony Pegram
sumber
Saya memahami selisih waktu runtime / kompilasi seperti pada contoh Anda tetapi bukankah konversi dari tipe khusus ke tipe dasar legal? Selain itu saya telah mengetik daftar dan saya beralih dari LinkLabel(tipe khusus) ke Control(tipe dasar).
TheVillageIdiot
2
Ya, mengonversi dari LinkLabel ke Kontrol adalah sah, tetapi itu tidak sama dengan apa yang terjadi di sini. Ini peringatan tentang konversi dari a LinkLabel[]ke Control[], yang masih legal, tetapi dapat memiliki masalah runtime. Semua yang telah berubah adalah cara array sedang direferensikan. Array itu sendiri tidak berubah. Lihat masalahnya? Array masih merupakan array dari tipe turunan. Referensi adalah melalui array dari tipe dasar. Oleh karena itu, kompilasi waktu legal untuk menetapkan elemen ke tipe dasar. Namun tipe runtime tidak akan mendukungnya.
Anthony Pegram
Dalam kasus Anda, saya tidak berpikir itu masalah, Anda cukup menggunakan array untuk menambah daftar kontrol.
Anthony Pegram
6
Jika ada yang bertanya-tanya mengapa array salah kovarian dalam C # di sini adalah penjelasan Eric Lippert : Itu ditambahkan ke CLR karena Java memerlukannya dan desainer CLR ingin dapat mendukung bahasa seperti Java. Kami kemudian naik dan menambahkannya ke C # karena itu di CLR. Keputusan ini cukup kontroversial pada saat itu dan saya tidak terlalu senang tentang itu, tetapi tidak ada yang bisa kita lakukan sekarang.
franssu
14

Saya akan mencoba menjelaskan jawaban Anthony Pegram.

Tipe generik adalah kovarian pada beberapa argumen tipe ketika ia mengembalikan nilai-nilai dari tipe tersebut (misalnya Func<out TResult>mengembalikan instance dari TResult, IEnumerable<out T>mengembalikan instance dari T). Artinya, jika sesuatu mengembalikan instance TDerived, Anda juga dapat bekerja dengan instance seperti seolah-olah berasal dari TBase.

Tipe generik bersifat contravarian pada beberapa argumen tipe ketika ia menerima nilai dari tipe tersebut (mis. Action<in TArgument>Menerima instance dari TArgument). Artinya, jika sesuatu membutuhkan contoh TBase, Anda juga dapat lulus dalam contoh TDerived.

Tampaknya cukup logis bahwa tipe generik yang menerima dan mengembalikan instance dari beberapa tipe (kecuali jika didefinisikan dua kali dalam tanda tangan tipe generik, misalnya CoolList<TIn, TOut>) tidak kovarian atau kontravarian pada argumen tipe yang sesuai. Misalnya, Listdidefinisikan dalam .NET 4 sebagai List<T>, bukan List<in T>atau List<out T>.

Beberapa alasan kompatibilitas mungkin menyebabkan Microsoft mengabaikan argumen itu dan membuat array kovarian pada argumen tipe nilai mereka. Mungkin mereka melakukan analisis dan menemukan bahwa kebanyakan orang hanya menggunakan array seolah-olah hanya dibaca (yaitu, mereka hanya menggunakan inisialisasi array untuk menulis beberapa data ke dalam array), dan, dengan demikian, kelebihannya melebihi kerugian yang disebabkan oleh kemungkinan runtime kesalahan ketika seseorang akan mencoba memanfaatkan kovarians saat menulis ke dalam array. Oleh karena itu diperbolehkan tetapi tidak dianjurkan.

Adapun pertanyaan asli Anda, list.ToArray()menciptakan baru LinkLabel[]dengan nilai-nilai disalin dari daftar asli, dan, untuk menyingkirkan (wajar) peringatan, Anda harus lulus dalam Control[]untuk AddRange. list.ToArray<Control>()akan melakukan pekerjaan: ToArray<TSource>menerima IEnumerable<TSource>sebagai argumennya dan kembali TSource[]; List<LinkLabel>mengimplementasikan read-only IEnumerable<out LinkLabel>, yang, berkat IEnumerablekovarian, dapat diteruskan ke metode menerima IEnumerable<Control>sebagai argumennya.

penartur
sumber
11

"Solusi" paling lurus ke depan

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

Sekarang karena Anda secara kovarian berubah List<LinkLabel>menjadi IEnumerable<Control>tidak ada lagi kekhawatiran karena tidak mungkin untuk "menambahkan" item ke yang dapat dihitung.

Chris Marisic
sumber
10

Peringatan ini disebabkan oleh kenyataan bahwa Anda secara teoritis bisa menambahkan Controlselain LinkLabeluntuk LinkLabel[]melalui Control[]referensi untuk itu. Ini akan menyebabkan pengecualian runtime.

Konversi terjadi di sini karena AddRangememerlukan a Control[].

Lebih umum, mengubah wadah dari tipe turunan ke wadah dari tipe dasar hanya aman jika Anda tidak dapat memodifikasi wadah dengan cara yang baru saja dijelaskan. Array tidak memenuhi persyaratan itu.

Stuart Golodetz
sumber
5

Akar penyebab masalah dijelaskan dengan benar dalam jawaban lain, tetapi untuk menyelesaikan peringatan, Anda selalu dapat menulis:

_list.ForEach(lnkLbl => flPanel.Controls.Add(lnkLbl));
Tim Williams
sumber
2

Dengan VS 2008, saya tidak mendapatkan peringatan ini. Ini harus baru untuk .NET 4.0.
Klarifikasi: menurut Sam Mackrill, Resharper yang menampilkan peringatan.

Compiler C # tidak tahu bahwa AddRangetidak akan memodifikasi array yang diteruskan ke sana. Karena AddRangememiliki parameter tipe Control[], secara teori bisa mencoba untuk menetapkan sebuah TextBoxke array, yang akan benar untuk array yang benar Control, tetapi sebenarnya array adalah array LinkLabelsdan tidak akan menerima tugas seperti itu.

Membuat array co-varian dalam c # adalah keputusan yang buruk dari Microsoft. Meskipun mungkin tampak ide yang bagus untuk dapat menetapkan array dari tipe turunan ke array dari tipe dasar, ini dapat menyebabkan kesalahan runtime!

Olivier Jacot-Descombes
sumber
2
Saya mendapat peringatan ini dari Resharper
Sam Mackrill
1

Bagaimana dengan ini?

flPanel.Controls.AddRange(_list.OfType<Control>().ToArray());
Sam Mackrill
sumber
2
Hasil yang sama dengan _list.ToArray<Control>().
jsuddsjr