C#泛型中的协变性

22

假设有一个接口 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始终为null。如果有人能解释两件事:

  • 为什么这样不起作用。
  • 我如何实现所需的效果。

非常感谢。谢谢!

7个回答

32
AMQuestion 实现 IQuestion 接口并不意味着 List<AMQuestion> 继承自 List<IQuestion>
由于这种强制转换是非法的,您的 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,这将破坏继承关系。

我一直认为使用泛型时,如果我有一个声明了 MyClass<T> 的类,那么 MyClass<IQuestion>MyClass<AMQuestion> 被认为是协变的,因此可以相互转换。 - Will Custode
6
只有在你能保证类型安全的情况下才可以。Rotem的编辑演示了一个例子,其中您可以传递不兼容的类型。但是,您可以“降级”某些内容:在这种情况下,您可以将您的List<AMQuestion>作为IEnumerable<IQuestion>处理。这是因为IEnumerable<IQuestion>不提供任何机制来_更改_它,因此始终保证类型安全。 编辑:当您使用outin关键字时,就会发挥作用。 - Chris Sinclair
4
我更愿意将它们称为“传统的”,而不是“陈词滥调的”。 - Eric Lippert
List<AMQuestion> 未实现 IList<IQuestion> 并不意味着不能有一个类继承自该类并实现该接口。 - supercat

12

为什么它不起作用: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>。由于只读列表不能添加任何奇怪的东西,因此可以正确地进行转换。


我很感谢关于只读的说明。如果我只需要读取访问权限,我会这样做的。谢谢! - Will Custode

7
问题在于List<AMQuestion>无法转换为IList<IQuestion>,因此使用as运算符无济于事。在这种情况下,显式转换意味着将AMQuestion强制转换为IQuestion
IList<IQuestion> nonTyped = typed.Cast<IQuestion>.ToList();

顺便提一下,你的标题中有“协变”这个术语。在 IList 中,类型不是协变的。这正是为什么转换不存在的原因。原因是 IList 接口在某些参数和某些返回值中都有 T,所以既不能使用 in 也不能使用 out 来处理 T。(@Sneftel 有一个很好的例子来说明为什么不允许进行此转换。)
如果您只需要从列表中读取数据,可以改用 IEnumerable
IEnumerable<IQuestion> = typed;

这将起作用是因为 IEnumerable<out T> 定义了 out,因为你不能将 T 作为参数传递给它。在代码中通常应该尽可能保持最弱的“承诺”,以使其可扩展。


3

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


2
“as”运算符在这里始终会返回null,因为不存在有效的转换 - 这是已定义的行为。您需要像这样转换或强制转换列表:
IList<IQuestion> nonTyped = typed.Cast<IQuestion>().ToList();

2
一个具有通用类型参数的类型只有在该通用类型在读访问中出现时才能协变,只有在写访问中出现时才能逆变。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!

1
因为List<T>不是密封类,所以可能存在一种类型,该类型将从List<AMQuestion>继承并实现IList<IQuestion>。除非您自己实现这样的类型,否则实际上几乎不可能存在这样的类型。尽管如此,例如说以下内容是完全合法的。
class SillyList : List<AMQuestion>, IList<IQuestion> { ... }

需要明确实现 IList<IQuestion> 接口的所有类型特定成员。因此,也可以合法地说:“如果此变量保存对派生自 List<AMQuestion> 类型的实例的引用,并且该实例的类型还实现了 IList<IQuestion>,则将引用转换为后者类型。”

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接