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



我有一个LinkLabel s ( IList<LinkLabel> private readonly列表。稍后,我将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. 这是一个用户控件,多个对象不会访问以设置标签, 因此,保持代码如此不会影响它。

意思是这个

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(作为基类型的数组(在本例中为控件数组(引用。将另一个作为数组Control的对象分配给数组也是编译时合法的。问题是数组实际上并不是控件数组。在运行时,它仍然是链接标签数组。因此,赋值或写入将引发异常。

我会试着澄清安东尼·佩格拉姆的答案。

泛型类型在返回所述类型的值时对某些类型参数是协变的(例如 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>,由于IEnumerable协方差,它可以传递给接受IEnumerable<Control>作为其参数的方法。

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

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

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

最直接的"解决方案">

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

现在,由于您正在协变地将List<LinkLabel>更改为IEnumerable<Control>因此不再有顾虑,因为无法将项目"添加"到可枚举项。

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

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

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

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

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

这个怎么样?

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

相关内容

  • 没有找到相关文章

最新更新