Совместный вариант преобразования массива из x в y может вызвать исключение во время выполнения - PullRequest
128 голосов
/ 02 января 2012

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

141 голосов
/ 02 января 2012

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

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

13 голосов
/ 30 января 2012

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

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

9 голосов
/ 02 января 2012

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

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

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

8 голосов
/ 01 августа 2012

Самое прямолинейное «решение»

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

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

5 голосов
/ 13 ноября 2013

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

_list.ForEach(lnkLbl => flPanel.Controls.Add(lnkLbl));
2 голосов
/ 02 января 2012

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

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

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

1 голос
/ 09 декабря 2013

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

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