15

为什么 C# 编译器不允许泛型集合中的多态类型 (T) 参数(即 List[T])?

以“A”和“B”类为例,其中“B”是“A”的子类

class A { }
class B : A { }

并考虑一个采用“A”类型列表的函数

void f(List<A> aL) { }

使用“B”类型的列表调用

List<B> bL = new List<B>();

f(bL);

给出以下错误

ERROR: cannot convert from List<B> to List<A>

违反了什么语义规则?

除了循环和投射每个元素(我想要一些糖)之外,还有一个“优雅”的意思吗?谢谢。

4

6 回答 6

12

以这个小例子说明为什么这不起作用。假设我们有另一个子C类型A

class A {}
class B : A {}
class C : A {}

那么显然,我可以将一个C对象放入List<A>列表中。但现在想象以下函数采用 A-list:

public void DoSomething (List<A> list)
{
    list.Add(new C());
}

如果您传递 aList<A>它会按预期工作,因为它C是放入 a 的有效类型List<A>,但是如果您传递 a List<B>,则不能将 aC放入该列表中。

对于这里发生的一般问题,请参阅数组的协变和逆变

于 2013-07-30T16:37:49.333 回答
12

List<B>根本不是List<A>. (我永远不确定在这种情况下什么是“协变”和什么“逆变”,所以我会坚持使用“子类型”。)考虑你这样做的情况:

void Fun(List<A> aa) {
    aa(new A());
}

var bb = new List<B>();
Fun(bb); // whoopsie

如果您想要做的事情被允许,则可以将 an 添加到显然不是类型安全A的 s 列表中。B

现在,显然可以安全地从列表中读取元素,这就是为什么 C# 允许您创建协变(即“只读”)接口- 这让编译器知道不可能通过它们导致这种损坏。如果您只需要读取权限,对于集合,通常的权限是IEnumerable<T>,因此在您的情况下,您可能只需要创建该方法:

void Fun(IEnumerable<A> aa) { ... }

并使用这些Enumerable方法 - 如果基础类型是 . 大多数应该优化List

不幸的是,由于 C# 泛型的工作方式,根本不能变体,只有接口。据我所知,所有的集合接口都比IEnumerable<T>“读写”更“丰富”。从技术上讲,您可以制作自己的协变包装接口,只公开您想要的读取操作。

于 2013-07-30T16:42:40.823 回答
3

B将集合传递给期望集合的方法本身并没有错A。但是,根据您要对集合执行的操作,有很多事情可能会出错。

考虑:

void f(List<A> aL)
{
    aL.(new A()); // oops! what happens here?
}

显然这里有一个问题:如果aL允许是 aList<B>那么这个实现将导致某种类型的运行时错误,要么当场发生,要么(更糟糕)如果稍后代码处理A我们作为 a 放入的实例B

编译器不允许您将 aList<B>用作 aList<B>以保持类型安全并保证您的代码不需要运行时检查就可以正确。请注意,这种行为与数组(不幸地)发生的行为不同——语言设计者的决定是一种权衡,他们在不同的场合做出了不同的决定:

void f(A[] arr)
{
    arr[0] = new A(); // exception thrown at runtime
}

f(new B[1]);
于 2013-07-30T16:38:14.183 回答
1

我认为您可能正在寻找“out”泛型修饰符,它允许两种泛型类型之间的协变。

http://msdn.microsoft.com/en-us/library/dd469487.aspx

该页面上发布的示例:

// Covariant delegate. 
public delegate R DCovariant<out R>();

// Methods that match the delegate signature. 
public static Control SampleControl()
{ return new Control(); }

public static Button SampleButton()
{ return new Button(); }

public void Test()
{            
    // Instantiate the delegates with the methods.
    DCovariant<Control> dControl = SampleControl;
    DCovariant<Button> dButton = SampleButton;

    // You can assign dButton to dControl 
    // because the DCovariant delegate is covariant.
    dControl = dButton;

    // Invoke the delegate.
    dControl(); 
}

我不确定 C# 当前是否支持其当前集合的协方差。

于 2013-07-30T16:39:03.060 回答
1

你的错误是 B 继承自 A;但List<B>不要继承自List<A>. List<A> != A;

你可以这样做:

List<A> aL = new List<A>();
aL.Add(new B());

f (aL)

您可以检测 void 中的类型f(List<A> list)

foreach(A a in list)
{
  if (a is B)
    //Do B stuff
  else
    //Do A stuff
}
于 2013-07-30T16:45:36.360 回答
0

您的问题与我的问题非常相似:答案是您不能这样做,因为它们是由模板类创建的不同类型并且它们不继承。你可以做的是:

f(bL.Cast<A>());
于 2013-07-30T16:35:56.287 回答