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

у меня есть 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. это пользовательский элемент управления и не будет доступен нескольким объектам для установки меток, таким образом, сохранение кода как такового не повлияет на него.

7 ответов


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

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. Таким образом, назначение или запись вызовет исключение.


я постараюсь уточнить ответ Энтони Пеграма.

Generic type ковариантен для некоторого аргумента типа, когда он возвращает значения указанного типа (например,Func<out TResult> возвращает экземпляры TResult, IEnumerable<out T> возвращает экземпляры T). То есть, если что-то возвращает экземпляры TDerived, вы можете также работать с такими экземплярами, как если бы они были TBase.

Generic type является контравариантным для некоторого аргумента типа, когда он принимает значения указанного типа (например,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[] to AddRange. list.ToArray<Control>() сделает работу:ToArray<TSource> принимает IEnumerable<TSource> в качестве аргумента и возвращает TSource[]; List<LinkLabel> реализует только для чтения IEnumerable<out LinkLabel>, который, благодаря IEnumerable ковариация, может быть передана методу принятия IEnumerable<Control> в качестве аргумента.


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

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

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


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

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

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


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

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

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

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

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


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

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