Перетворення ко-варіантного масиву з 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для масиву. Проблема полягає в тому, що масив насправді не є масивом Controls. Під час виконання це все ще масив LinkLabels. Таким чином, призначення або запис буде винятком.


Я розумію різницю у часі виконання / компіляції, як у вашому прикладі, але чи не перетворення від спеціального типу до базового типу легальне? Більше того, я набрав список, і я переходжу від LinkLabel(спеціалізованого типу) до Control(базового типу).
TheVillageIdiot

2
Так, перехід від LinkLabel до Control є законним, але це не те саме, що відбувається тут. Це попередження про перетворення з а LinkLabel[]в Control[], що все ще є законним, але може мати проблеми з часом виконання. Все, що змінилося - це спосіб посилання на масив. Сам масив не змінюється. Бачите проблему? Масив все ще є масивом похідного типу. Посилання здійснюється через масив базового типу. Отже, час компіляції законно присвоїти йому елемент базового типу. Тим не менш, тип виконання не підтримав би його.
Ентоні Пеграм

У вашому випадку я не думаю, що це проблема, ви просто використовуєте масив для додавання до списку елементів управління.
Ентоні Пеграм

6
Якщо когось цікавить, чому масиви є коваріантними в C # , пояснення Еріка Ліпперта : Це було додано до CLR, оскільки цього вимагає Java, і дизайнери CLR хотіли мати можливість підтримувати мови, схожі на Java. Потім ми додали його до C #, оскільки це було в CLR. Це рішення було досить суперечливим на той час, і я не дуже радий цьому, але ми нічого з цим зараз не можемо зробити.
franssu

14

Спробую уточнити відповідь Ентоні Пеграма.

Загальний тип є коваріантним для аргументу типу, коли він повертає значення зазначеного типу (наприклад, Func<out TResult>повертає екземпляри TResult, IEnumerable<out T>повертає екземпляри T). Тобто, якщо щось повертає екземпляри TDerived, ви також можете працювати з такими екземплярами, як ніби вони були TBase.

Загальний тип є протилежним для аргументу деяких типів, коли він приймає значення вказаного типу (наприклад, Action<in TArgument>приймає екземпляри TArgument). Тобто, якщо щось потребує екземплярів TBase, ви можете також передавати їх в екземпляри TDerived.

Здається цілком логічним, що загальні типи, які приймають і повертають екземпляри якогось типу (якщо вони не визначені двічі в підписі загального типу, наприклад CoolList<TIn, TOut>), не є коваріантними та не противаріантними у відповідному аргументі типу. Наприклад, Listвизначено в .NET 4 як List<T>, не List<in T>або List<out T>.

Деякі причини сумісності могли призвести до того, що Microsoft ігнорує цей аргумент та робить масиви коваріантними на аргументі типу значень. Можливо, вони провели аналіз і виявили, що більшість людей використовують масиви лише так, як якщо б вони були прочитані лише зараз (тобто вони використовують лише ініціалізатори масиву для запису деяких даних у масив), і, як такий, переваги переважають за недоліки, спричинені можливим режимом виконання помилки, коли хтось спробує використати коваріацію під час запису в масив. Отже, це дозволено, але не заохочується.

Що стосується вашого оригінального запитання, ви list.ToArray()створюєте нове LinkLabel[]зі значеннями, скопійованими з оригінального списку, і, щоб позбутися (розумного) попередження, вам потрібно буде перейти Control[]до нього AddRange. list.ToArray<Control>()зробить роботу: ToArray<TSource>приймає IEnumerable<TSource>як свій аргумент і повертає TSource[]; List<LinkLabel>реалізує лише для читання IEnumerable<out LinkLabel>, які, завдяки IEnumerableковаріації, можуть бути передані методу, що приймає IEnumerable<Control>його аргументом.


11

Найпростіше "рішення"

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

Тепер , так як ви коваріантного зміни List<LinkLabel>в IEnumerable<Control>немає більше проблем , так як це не представляється можливим "додати" елемент в Перечіслімий.


10

Попередження пов'язане з тим , що ви могли б теоретично додати Controlінше , ніж LinkLabelдо LinkLabel[]через Control[]посилання на нього. Це може спричинити виняток із часу виконання.

Перетворення відбувається тут, тому що AddRangeзаймає а Control[].

Більш загально, перетворення контейнера похідного типу в контейнер базового типу є безпечним лише в тому випадку, якщо ви не зможете згодом модифікувати контейнер у визначеному способі. Масиви не відповідають цій вимозі.


5

Першопричину проблеми правильно описано в інших відповідях, але для усунення попередження завжди можна написати:

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

2

З VS 2008 я не отримую цього попередження. Це повинно бути новим для .NET 4.0.
Уточнення: за словами Сема Макрілла, це Resharper відображає попередження.

Компілятор C # не знає, що AddRangeне змінить переданий йому масив. Оскільки AddRangeмає параметр типу Control[], він теоретично міг би спробувати призначити TextBoxмасив a , що було б абсолютно правильним для істинного масиву Control, але насправді масив є масивом LinkLabelsі не прийме такого призначення.

Здійснення ко-варіанту масивів у c # було поганим рішенням Microsoft. Хоча може здатися, що в першу чергу можна присвоїти масив похідному типу масиву базового типу, це може призвести до помилок виконання!


2
Я отримую це попередження від Resharper
Сем Макрілл

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.