为什么我可以抛弃IList <t>的不变性?</t>

时间:2011-10-26 18:45:50

标签: c# c#-4.0 type-conversion generic-variance

目前,我正准备为同事介绍C#中新的通用差异功能。为了缩短故事,我写了以下几行:

IList<Form> formsList = new List<Form> { new Form(), new Form() };
IList<Control> controlsList = formsList;

是的,这当然是不可能的,因为IList(Of T)是不变的(至少我的想法)。编译器告诉我:

  

无法隐式转换类型   System.Collections.Generic.IList<System.Windows.Forms.Form>来   System.Collections.Generic.IList<System.Windows.Forms.Control>。一个   存在显式转换(您是否错过了演员?)

嗯,这是否意味着我可以强制进行显式转换?我刚尝试过:

IList<Form> formsList = new List<Form> { new Form(), new Form() };
IList<Control> controlsList = (IList<Control>)formsList;

而且......它编译!这是否意味着我可以抛弃不变性? - 至少编译器没问题,但我只是将前编译时错误转为运行时错误:

  

Unable to cast object of type 'System.Collections.Generic.List`1[System.Windows.Forms.Form]' to type 'System.Collections.Generic.IList`1[System.Windows.Forms.Control]'.

我的问题:为什么我可以将IList<T>(或我的实验的任何其他不变界面)的不变性投射出来?我是否真的抛弃了不变性,或者这里发生了什么样的转换(因为IList(Of Form)IList(Of Control)是完全不相关的)?这是C#的一个黑暗角落,我不知道?

2 个答案:

答案 0 :(得分:6)

基本上,类型可以实现IList<Control> 以及 IList<Form>所以可能用于转换为成功 - 所以编译器暂时让它通过(除此之外:它可能在这里更聪明并产生警告,因为它知道引用对象的具体类型,但不知道。我不认为这是合适的产生编译器错误,因为它不是实现新接口的类型的重大变化。)

作为这种类型的一个例子:

public class EvilList : IList<Form>, IList<Control> { ... }

在运行时发生的只是CLR类型检查。您看到的例外情况表示此操作失败。

为演员生成的IL是:

castclass [mscorlib]System.Collections.Generic.IList`1<class [System.Windows.Forms]System.Windows.Forms.Control>

来自MSDN

  

castclass指令尝试转换对象引用(类型   O)在堆栈顶部到指定的类。新类由。指定   指示所需类的元数据标记。如果上课的话   堆栈顶部的对象不实现新类   (假设新类是一个接口)并且不是派生类   新类然后抛出InvalidCastException。如果是对象   reference是一个null引用,castclass成功并返回new   object作为空引用。

     

如果无法将obj强制转换为类,则抛出InvalidCastException。

答案 1 :(得分:1)

我怀疑在这种情况下,如果您尝试将新的TextBlock添加到controlsList中,则会抛出运行时异常。 TextBlock将符合controlsList的合同,但不符合formsList。

IList<Form> formsList = new List<Form> { new Form(), new Form() }; 
IList<Control> controlsList = (IList<Control>)formsList; 
controlsList.Add(New TextBlock); // Should throw at runtime.

在这种情况下,类型安全不变通常会将其作为运行时异常。在这种情况下,您可以安全地将controlsList声明为IEnumerable而不是IList(假设.Net 4.0),因为IEnumerable被声明为协变(IEnumerable)。这解决了尝试向控件列表添加错误类型的问题,因为.add(和其他输入方法)在out接口中不可用。