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

Posted

技术标签:

【中文标题】从 x 到 y 的协变数组转换可能会导致运行时异常【英文标题】:Co-variant array conversion from x to y may cause run-time exception 【发布时间】:2012-02-01 00:35:01 【问题描述】:

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

foreach(var s in strings)

    _list.Add(new LinkLabelText=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

在 C# 中,您可以将对象数组(在您的示例中为 LinkLabels)引用为基本类型的数组(在此示例中为控件数组)。将Controlanother 对象分配给数组也是编译时合法的。问题在于数组实际上并不是控件数组。在运行时,它仍然是一个 LinkLabels 数组。因此,赋值或写入将引发异常。

【讨论】:

我理解运行时/编译时间的差异,但从特殊类型到基本类型的转换不合法吗?此外,我输入了列表,我将从LinkLabel(专业类型)变为Control(基本类型)。 是的,从 LinkLabel 转换为 Control 是合法的,但这与这里发生的情况不同。这是关于从LinkLabel[] 转换为Control[] 的警告,这仍然是合法的,但可能存在运行时问题。改变的只是数组被引用的方式。数组本身没有改变。看到问题了吗?该数组仍然是派生类型的数组。引用是通过基本类型的数组进行的。因此,在编译时为它分配一个基本类型的元素是合法的。然而运行时类型不支持它。 在您的情况下,我认为这不是问题,您只是使用数组添加到控件列表中。 如果有人想知道为什么数组在 C# 中是错误的协变,这里是 Eric Lippert's explanation:它被添加到 CLR 是因为 Java 需要它并且 CLR 设计者希望能够支持类似 Java语言。然后我们将它添加到 C# 中,因为它在 CLR 中。这个决定在当时颇有争议,我对此并不十分高兴,但现在我们无能为力。【参考方案2】:

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

泛型类型在返回所述类型的值时在某些类型参数上是协变的(例如,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】:

最直接的“解决方案”

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

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

【讨论】:

【参考方案4】:

警告是因为理论上您可以通过 Control[] 引用将 Control 添加到 LinkLabelLinkLabel[] 之外。这会导致运行时异常。

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

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

【讨论】:

【参考方案5】:

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

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

【讨论】:

【参考方案6】:

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

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

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

【讨论】:

我从 Resharper 收到此警告【参考方案7】:

这个怎么样?

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

【讨论】:

_list.ToArray&lt;Control&gt;()的结果相同。

以上是关于从 x 到 y 的协变数组转换可能会导致运行时异常的主要内容,如果未能解决你的问题,请参考以下文章

JAVA中的协变与逆变

当结构是在 C# 中转换的通用对象的协变类型时,结构无法转换为对象的任何解决方法?

Typescript 中的协变和逆变

详解C#的协变和逆变

Java泛型之通配符

(56)C#里的协变(covariant)和逆变(contravariant)