从 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. 这是什么意思?
  2. 这是一个用户控件,不会被多个对象访问以设置标签, 所以保持代码本身不会影响它。
36403 次浏览

它的意思是

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 数组)。将 另一个对象(Control)分配给数组也是在编译时合法的。问题是数组 实际上不是“控件”的数组。在运行时,它仍然是一个 LinkLabels 数组。因此,赋值或写操作将引发异常。

这个警告是由于理论上可以通过对 Control[]的引用向 LinkLabel[]添加 Control而不是 LinkLabel。这将导致运行时异常。

转换发生在这里,因为 AddRange采用 Control[]

更一般地说,只有当您随后不能以刚才概述的方式修改容器时,将派生类型的容器转换为基类型的容器才是安全的。数组不能满足这个要求。

在 VS2008中,我没有收到这个警告。这对.NET 4.0来说一定是新的。
澄清: 根据山姆 · 麦克里尔的说法,是 Resharper 发出了警告。

C # 编译器不知道 AddRange不会修改传递给它的数组。因为 AddRange有一个 Control[]类型的参数,所以理论上它可以尝试给数组赋值一个 TextBox,这对于一个真正的 Control数组来说是完全正确的,但是这个数组实际上是一个 LinkLabels数组,不会接受这样的赋值。

在 c # 中使用数组协变是微软的一个错误决定。尽管首先将派生类型的数组分配给基类型的数组似乎是一个好主意,但这可能导致运行时错误!

我会尽力澄清安东尼 · 佩格拉姆的回答。

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

最直接的“解决方案”

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

现在,既然您正在协变地将 List<LinkLabel>更改为 IEnumerable<Control>,那么就没有更多的问题了,因为不可能将项“添加”到可枚举数中。

问题的根本原因在其他答案中有正确的描述,但是为了解决这个警告,你可以这样写:

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

这个怎么样?

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