Ковариантное преобразование массива из x в y может вызвать исключение во время выполнения

У меня есть private readonly список LinkLabel (IList<LinkLabel>). Позже я добавлю LinkLabel в этот список и добавлю эти метки к 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

Что это значит, это

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

В С# вам разрешено ссылаться на массив объектов (в вашем случае LinkLabels) в виде массива базового типа (в данном случае в виде массива элементов управления). Также время компиляции разрешено назначать другому массиву Control другому объекту. Проблема в том, что массив фактически не является массивом элементов управления. Во время выполнения это все еще массив LinkLabels. Таким образом, назначение или запись генерирует исключение.

Ответ 2

Я попытаюсь разъяснить ответ Энтони Пегем.

Общий тип является ковариантным для некоторого аргумента типа, когда он возвращает значения указанного типа (например, 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> в качестве аргумента.

Ответ 3

Предупреждение связано с тем, что теоретически можно добавить Control, кроме LinkLabel, в LinkLabel[] через ссылку Control[] к нему. Это приведет к исключению во время выполнения.

Здесь происходит преобразование, потому что AddRange принимает значение Control[].

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

Ответ 4

Самое прямое "решение"

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

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

Ответ 5

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

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

Ответ 6

С VS 2008 я не получаю это предупреждение. Это должно быть новым для .NET 4.0.
Уточнение: по словам Сэма Макрилла, это Resharper, который отображает предупреждение.

Компилятор С# не знает, что AddRange не будет изменять переданный ему массив. Так как AddRange имеет параметр типа Control[], теоретически можно попытаться присвоить массив TextBox, что было бы совершенно правильным для истинного массива Control, но массив на самом деле представляет собой массив из LinkLabels и не будет принимать такое назначение.

Создание совместного использования массивов в С# было плохим решением Microsoft. Хотя может показаться хорошей идеей иметь возможность присвоить массив производного типа массиву базового типа в первую очередь, это может привести к ошибкам во время выполнения!

Ответ 7

Как насчет этого?

flPanel.Controls.AddRange(_list.OfType<Control>().ToArray());