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

145

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

Ответы:

156

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

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) как на массив базового типа (в данном случае как на массив элементов управления). Также во время компиляции допустимо присвоить массиву другой объект, являющийся a Control. Проблема в том, что массив на самом деле не является массивом элементов управления. Во время выполнения это все еще массив LinkLabels. Таким образом, присвоение или запись вызовет исключение.

Энтони Пеграм
источник
Я понимаю разницу во времени выполнения / компиляции, как в вашем примере, но разве преобразование из специального типа в базовый тип законно? Кроме того, я набрал список и перехожу от LinkLabel(специализированный тип) к Control(базовый тип).
TheVillageIdiot 02
2
Да, преобразование LinkLabel в Control является законным, но это не то же самое, что здесь. Это предупреждение о преобразовании из a LinkLabel[]в Control[], которое все еще разрешено, но может иметь проблемы во время выполнения. Все, что изменилось, - это способ ссылки на массив. Сам массив не меняется. Видите проблему? Массив по-прежнему является массивом производного типа. Ссылка осуществляется через массив базового типа. Следовательно, во время компиляции можно назначить элемент базового типа. Тем не менее, тип среды выполнения не поддерживает его.
Энтони Пеграм
В вашем случае я не думаю, что это проблема, вы просто используете массив для добавления в список элементов управления.
Энтони Пеграм
8
Если кому-то интересно, почему массивы в 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>not 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>качестве аргумента.

Penartur
источник
11

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

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

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

Крис Маришич
источник
10

Предупреждение связано с тем, что теоретически вы могли бы добавить к объекту ссылку, отличную Controlот a . Это вызовет исключение во время выполнения.LinkLabelLinkLabel[]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
Я получил это предупреждение от Решарпера
Сэм Макрил
1

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

flPanel.Controls.AddRange(_list.OfType<Control>().ToArray());
Сэм Макрил
источник
2
Тот же результат, что и _list.ToArray<Control>().
jsuddsjr