Ковариантное преобразование массива из 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());