c#泛型的协方差

本文关键字:方差 泛型 | 更新日期: 2023-09-27 18:09:24

给定一个接口IQuestion和该接口AMQuestion的实现,假设如下示例:

List<AMQuestion> typed = new List<AMQuestion>();
IList<IQuestion> nonTyped = typed;

如预期的那样,这个例子产生一个编译错误,说这两个不是同一类型。但它表明存在显式转换。所以我把它改成这样:

List<AMQuestion> typed = new List<AMQuestion>();
IList<IQuestion> nonTyped = typed as IList<IQuestion>;

然后编译,但在运行时,nonTyped始终为空。如果有人能解释两件事:

  • 为什么不工作
  • 如何达到我想要的效果

非常感谢。谢谢你!

c#泛型的协方差

AMQuestion实现IQuestion接口的事实并不转化为List<IQuestion>派生的List<AMQuestion>

因为此强制转换是非法的,所以您的as操作符返回null

必须将每个项单独强制转换为:

IList<IQuestion> nonTyped = typed.Cast<IQuestion>().ToList();

关于您的评论,请考虑以下代码,其中包含常见的动物示例:

//Lizard and Donkey inherit from Animal
List<Lizard> lizards = new List<Lizard> { new Lizard() };
List<Donkey> donkeys = new List<Donkey> { new Donkey() };
List<Animal> animals = lizards as List<Animal>; //let's pretend this doesn't return null
animals.Add(new Donkey()); //Reality unravels!

如果允许将List<Lizard>强制转换为List<Animal>,那么理论上可以在该列表中添加新的Donkey,这将破坏继承。

为什么不起作用:如果值的动态类型不能转换为目标类型,则as返回null,而List<AMQuestion>不能转换为IList<IQuestion>

但是为什么不能呢?好吧,检查一下:

List<AMQuestion> typed = new List<AMQuestion>();
IList<IQuestion> nonTyped = typed as IList<IQuestion>;
nonTyped.Add(new OTQuestion());
AMQuestion whaaaat = typed[0];

IList<IQuestion>说"你可以添加任何IQuestion给我"。但如果它是List<AMQuestion>的话,这是一个无法兑现的承诺。

现在,如果您不想添加任何东西,只需将其视为IQuestion兼容的东西的集合,那么最好的方法就是将其与List.AsReadOnly强制转换为IReadOnlyList<IQuestion>。由于只读列表不能添加奇怪的内容,因此可以正确地对其进行强制类型转换。

问题是List<AMQuestion>不能被强制转换为IList<IQuestion>,所以使用as运算符没有帮助。在这种情况下,显式转换意味着将AMQuestion转换为IQuestion:

IList<IQuestion> nonTyped = typed.Cast<IQuestion>.ToList();

顺便说一下,你的标题中有"协方差"这个词。在IList中,类型是而不是协变。这就是演员阵容不存在的原因。原因是IList接口在某些参数中有T,在某些返回值中有,所以T既不能用in也不能用out。(@Sneftel有一个很好的例子来说明为什么不允许这种类型。)

如果你只需要从列表中读取,你可以使用IEnumerable代替:

IEnumerable<IQuestion> = typed;

这将工作,因为IEnumerable<out T>已经定义了out,因为你不能传递T作为参数。你通常应该在你的代码中做出最弱的"承诺",以保持它的可扩展性。

IList<T>T不协变;这是不可能的,因为接口在"输入"位置定义了接受T类型值的函数。然而,IEnumerable<T>T的协变。如果您可以限制您的类型为IEnumerable<T>,您可以这样做:

List<AMQuestion> typed = new List<AMQuestion>();
IEnumerable<IQuestion> nonTyped = typed;

这不会对列表进行任何转换。

您不能将List<AMQuestion>转换为List<IQuestion>的原因(假设AMQuestion实现了该接口)是,必须对List<T>.Add等函数进行多次运行时检查,以确保您确实添加了AMQuestion

如果不存在有效的强制类型转换,则"as"操作符将始终返回null -这是定义的行为。您必须像这样转换或强制转换列表:

IList<IQuestion> nonTyped = typed.Cast<IQuestion>().ToList();

具有泛型类型参数的类型只能是协变的,如果该泛型类型只出现在读访问中,则只能是逆变的,如果它只出现在写访问中。IList<T>允许对T类型的值进行读和写访问,所以它不能是变体!

假设允许将List<AMQuestion>赋值给IList<IQuestion>类型的变量。现在让我们实现一个class XYQuestion : IQuestion,并将该类型的值插入到IList<IQuestion>中,这看起来完全合法。这个列表仍然引用List<AMQuestion>,但是我们不能将XYQuestion插入List<AMQuestion> !因此,这两种列表类型不兼容赋值操作。

IList<IQuestion> list = new List<AMQuestion>(); // Not allowed!
list.Add(new XYQuestion()); // Uuups!

因为List<T>不是一个密封类,所以有可能存在继承List<AMQuestion>并实现IList<IQuestion>的类型。除非您自己实现这样的类型,否则实际上不太可能存在这样的类型。尽管如此,完全可以这样说,例如

class SillyList : List<AMQuestion>, IList<IQuestion> { ... }

并显式地实现IList<IQuestion>的所有特定类型的成员。因此,也可以完全合理地说:"如果这个变量持有对从List<AMQuestion>派生的类型的实例的引用,并且如果该实例的类型也实现了IList<IQuestion>,则将引用转换为后一种类型。"