从x到y的协变量数组转换可能会导致运行时异常


142

我有一个s()的private readonly列表。稍后,我将s 添加到此列表,并将这些标签添加到如下所示:LinkLabelIList<LinkLabel>LinkLabelFlowLayoutPanel

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. 这是一个用户控件,不会被多个对象访问以设置标签,因此,保持这样的代码不会影响它。

Answers:


154

这是什么意思

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)引用为基本类型的数组(在本例中为Controls数组)。将另一个对象a 分配给Control数组也是编译时合法的。问题在于该数组实际上不是控件数组。在运行时,它仍然是LinkLabels的数组。这样,赋值或写操作将引发异常。


我了解您的示例中的运行时/编译时差,但是从特殊类型到基本类型的转换合法吗?此外,我已经键入列表,并且我将从LinkLabel(专用类型)转到Control(基本类型)。
TheVillageIdiot'1

2
是的,从LinkLabel转换为Control是合法的,但这与此处发生的情况不同。这是警告有关从转换LinkLabel[]Control[],这仍然是合法的,但可以有一个运行时的问题。唯一改变的是引用数组的方式。数组本身未更改。看到问题了吗?该数组仍然是派生类型的数组。引用是通过基本类型的数组进行的。因此,将基本类型的元素分配给它在编译时是合法的。但是,运行时类型不支持它。
安东尼·佩格拉姆

就您而言,我认为这不是问题,您只是在使用数组添加到控件列表中。
安东尼·佩格拉姆

6
如果有人想知道为什么数组在C#中错误地协变,这就是Eric Lippert的解释它被添加到CLR中是因为Java要求它并且CLR设计者希望能够支持类Java语言。然后,我们将其添加到C#中,因为它在CLR中。当时的决定颇具争议,我对此并不满意,但现在我们无能为力。
franssu 2014年

14

我将尝试澄清Anthony Pegram的答案。

当泛型类型返回该类型的值(例如,Func<out TResult>返回的实例TResultIEnumerable<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[]AddRangelist.ToArray<Control>()将完成这项工作:ToArray<TSource>接受IEnumerable<TSource>作为参数并返回TSource[]; List<LinkLabel>实现只读IEnumerable<out LinkLabel>,由于IEnumerable协方差,可以将其传递给接受IEnumerable<Control>作为其参数的方法。


11

最直接的“解决方案”

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

现在,由于您要进行协变更改List<LinkLabel>IEnumerable<Control>因此不再需要担心,因为不可能将项目“添加”到枚举中。


10

该警告是由于这样的事实,你可以添加理论上一个Control比其他LinkLabelLinkLabel[]通过Control[]引用。这将导致运行时异常。

转换发生在这里,因为AddRange需要Control[]

更一般而言,仅当您随后无法以刚才概述的方式修改容器时,将派生类型的容器转换为基本类型的容器才是安全的。数组不满足该要求。


5

该问题的根本原因已在其他答案中正确描述,但是要解决该警告,您始终可以这样写:

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

2

在VS 2008中,我没有收到此警告。这对于.NET 4.0必须是新的。
澄清:根据Sam Mackrill的说法,Resharper会显示警告。

C#编译器不知道AddRange不会修改传递给它的数组。由于AddRange参数的类型为Control[],因此理论上可以尝试将a分配TextBox给该数组,这对于的真实数组是完全正确的Control,但实际上该数组是的数组,LinkLabels并且将不接受此类分配。

在C#中使数组协变是Microsoft的错误决定。首先可以将派生类型的数组分配给基本类型的数组似乎是一个好主意,但这会导致运行时错误!


2
我从ReSharper的这个警告
山姆Mackrill

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.