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

164

我有一个private readonly列表,其中包含LinkLabels(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. 这是什么意思?
  2. 这是一个用户控件,不会被多个对象访问以设置标签,因此保留代码不会影响它。
7个回答

167

它的意思是这样的

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#中,你可以将一组对象(在你的情况下,是LinkLabel)引用为基类型的数组(在这种情况下,引用为Controls的数组)。编译时也可以合法地将另一个作为Control的对象赋值给这个数组。问题是数组实际上并不是由Controls组成的数组。在运行时,它仍然是一个LinkLabels数组。因此,这样的赋值或写操作会抛出异常。

我理解你举的例子中运行时和编译时的差异,但是从特殊类型到基础类型的转换不合法吗?此外,我有一个已经声明类型的列表,并且我正在从“LinkLabel”(特定类型)转换为“Control”(基础类型)。 - TheVillageIdiot
6
是的,将LinkLabel转换为Control是合法的,但这与此处发生的情况不同。这是在警告从“LinkLabel []”转换为“Control []”,这仍然是合法的,但可能会出现运行时问题。所有改变的只是数组的引用方式,而数组本身并未改变。明白了吗?该数组仍然是派生类型的数组,引用是通过基础类型的数组进行的。因此,可以在编译时将其赋予基础类型的元素,但运行时类型不支持它。 - Anthony Pegram
在您的情况下,我认为这不是问题,您只是使用数组来添加到控件列表中。 - Anthony Pegram
15
如果有人想知道为什么在C#中数组是错误的协变,这里有Eric Lippert的解释它被添加到CLR中是因为Java需要它,而CLR设计者希望能够支持类似Java的语言。 然后我们将其添加到C#中,因为它在CLR中。 这个决定当时引起了很大争议,我对此不是很满意,但现在我们无能为力了。 - franssu

14
我将尝试澄清Anthony Pegram的答案。
泛型类型在某些类型参数上是协变的,当它返回所述类型的值时(例如,Func<out TResult> 返回 TResult 实例,IEnumerable<out T> 返回 T 实例)。也就是说,如果某个东西返回了 TDerived 的实例,那么您可以像处理 TBase 实例一样使用这些实例。
泛型类型在某些类型参数上是逆变的,当它接受所述类型的值时(例如,Action<in TArgument> 接受 TArgument 实例)。也就是说,如果某个东西需要 TBase 实例,那么您可以像传递 TDerived 实例一样传递这些实例。
似乎很合理,当泛型类型既接受又返回某种类型(除非在泛型类型签名中定义了两次,例如:CoolList<TIn, TOut>),则不会在相应类型参数上是协变或者是逆变。例如,在 .NET 4 中,List 被定义为 List<T>,而不是 List<in T>List<out T>
由于一些兼容性原因,微软可能忽略了该参数并使数组协变为它们的值类型参数。也许他们进行了分析,并发现大多数人只是将数组用作只读数组(也就是说,他们只使用数组初始化器将某些数据写入数组),因此优点超过了由于在向数组编写时尝试使用协变而引起的可能运行时错误而导致的缺点。因此虽然允许但不建议这样做。
至于您最初的问题,list.ToArray() 创建一个新的 LinkLabel[],其中的值从原始列表中复制,并且为了摆脱(合理的)警告,您需要传递 Control[]AddRangelist.ToArray<Control>() 可以完成工作:ToArray<TSource> 接受 IEnumerable<TSource> 作为其参数,并返回 TSource[]; List<LinkLabel> 实现只读的 IEnumerable<out LinkLabel>,这要归功于 IEnumerable 协变性,可以将其传递给接受 IEnumerable<Control> 作为其参数的方法。

12

警告的原因是您理论上可以通过对Control[]引用来将除LinkLabel以外的Control添加到LinkLabel[]中。这会导致运行时异常。

转换发生在此处,因为AddRange接受一个Control[]参数。

更一般地说,如果您无法像刚才概述的那样以此修改容器,则将派生类型的容器转换为基类型的容器仅在安全的情况下才有效。数组不满足该要求。


11

最直接的“解决方案”:

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

现在,由于您将 List<LinkLabel> 协变地转换为 IEnumerable<Control>,因此不再有问题,因为无法向可枚举对象中“添加”项目。


5
问题的根本原因在其他答案中已经正确描述,但为了解决这个警告,您可以始终编写以下内容:
_list.ForEach(lnkLbl => flPanel.Controls.Add(lnkLbl));

3

使用VS 2008时,我没有收到这个警告。这一定是.NET 4.0新的特性。
澄清:根据Sam Mackrill的说法,这是Resharper显示的警告。

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

在C#中使数组共变是Microsoft的一个糟糕决定。虽然在第一次将派生类型的数组分配给基础类型的数组似乎是一个好主意,但这可能导致运行时错误!


5
好的,我会尽力进行翻译。以下是需要翻译的内容:"I get this warning from Resharper" - Sam Mackrill

1
这个怎么样?
flPanel.Controls.AddRange(_list.OfType<Control>().ToArray());

3
_list.ToArray<Control>() 产生相同的结果。 - jsuddsjr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接