为什么我不能将列表<Derived>分配给列表<Base>?

问题描述 投票:0回答:6

我定义了以下类:

public abstract class AbstractPackageCall
    {

     ...

    }

我还定义了这个类的子类:

class PackageCall : AbstractPackageCall
    {

      ...
    }

AbstractPackageCall

还有其他几个子类

现在我要拨打以下电话:

 List<AbstractPackageCall> calls = package.getCalls();

但我总是遇到这个例外:

Error   13  Cannot implicitly convert type 'System.Collections.Generic.List<Prototype_Concept_2.model.PackageCall>' to 'System.Collections.Generic.List<Prototype_Concept_2.model.AbstractPackageCall>' 

这里有什么问题吗?这是方法 Package#getCalls

 internal List<PackageCall> getCalls()
        {
            return calls;
        }
c# .net generics covariance
6个回答
61
投票

了解为什么不允许这样做的最简单方法是以下示例:

abstract class Fruit
{
}

class Apple : Fruit
{
}

class Banana : Fruit
{
}

// This should intuitively compile right? Cause an Apple is Fruit.
List<Fruit> fruits = new List<Apple>();

// But what if I do this? Adding a Banana to a list of Apples
fruits.Add(new Banana());

最后一条语句会破坏 .NET 的类型安全。

但是数组确实允许这样做:

Fruit[] fruits = new Apple[10]; // This is perfectly fine

但是,将

Banana
放入
fruits
中仍然会破坏类型安全,因此 .NET 必须对每个数组插入进行类型检查,如果它实际上不是
Apple
,则抛出异常。这可能会对性能造成(小)影响,但是可以通过围绕任一类型创建
struct
包装器来避免这种情况,因为值类型不会发生此检查(因为它们不能从任何类型继承)。起初,我不明白为什么做出这个决定,但你会经常遇到为什么这很有用。最常见的是
String.Format
,它接受
params object[]
并且任何数组都可以传递到其中。

但在 .NET 4 中,存在类型安全的协变/逆变,它允许您进行一些类似的分配,但前提是它们被证明是安全的。什么是可证明安全的?

IEnumerable<Fruit> fruits = new List<Apple>();

上面的代码适用于 .NET 4,因为

IEnumerable<T>
变成了
IEnumerable<out T>
out
意味着
T
只能来自 fruits
out
并且 IEnumerable<out T> 上根本没有
 任何将 
T
 作为参数的方法,所以你永远不会错误地传递一个
Banana
进入IEnumerable<Fruit>

逆变大致相同,但我总是忘记它的具体细节。毫不奇怪,现在类型参数上有

in

 关键字。


3
投票
现在如果您想拨打以下电话:

List<PackageCall> calls = package.getCalls(); // select only AbstractPackageCall items List<AbstractPackageCall> calls = calls.Select(); calls.Add(new AnotherPackageCall());

我称之为概括。

此外,您可以使用更具体的:

List<PackageCall> calls = package.getCalls(); // select only AnotherPackageCall items List<AnotherPackageCall> calls = calls.Select(); calls.Add(new AnotherPackageCall());

使用扩展方法实现此功能:

public static class AbstractPackageCallHelper { public static List<U> Select(this List<T> source) where T : AbstractPackageCall where U : T { List<U> target = new List<U>(); foreach(var element in source) { if (element is U) { target.Add((U)element); } } return target; } }
    

2
投票
为什么你的尝试不起作用

您要求编译器将

List<PackageCall>

 视为 
List<AbstractPackageCall>
,但事实并非如此。另一个问题是,每个 
PackageCall
 实例实际上都是 
AbstractPackageCall

什么会起作用

var calls = package.getCalls().Cast<AbstractPackageCall>().ToList();

为什么你的尝试永远不会成功

尽管

package.getCalls()

 返回值中的每一项都源自 
AbstractPackageCall
,但我们不能将整个列表视为 
List<AbstractPackageCall>
。如果我们可以的话,将会发生以下情况:

var calls = package.getCalls(); // calls is List<PackageCall> List<AbstractPackageCalls> apcs = calls; // ILLEGAL, but assume we could do it apcs.Add(SomeOtherConcretePackageCall()); // BOOM!

如果我们能做到这一点,那么我们可以将

SomeOtherConcretePackageCall

 添加到 
List<PackageCall>


1
投票
您无法执行此转换,因为

List<AbstractPackageCall>

 可以包含从 
AbstractPackageCall
 派生的任何内容,而 
List<PackageCall>
 不能——它只能包含从 
PackageCall
 派生的内容。

考虑是否允许这种演员阵容:

class AnotherPackageCall : AbstractPackageCall { } // ... List<AbstractPackageCall> calls = package.getCalls(); calls.Add(new AnotherPackageCall());

您刚刚将

AnotherPackageCall

 添加到 
List<PackageCall>
 中 - 但 
AnotherPackageCall
 并非源自 
PackageCall
!  这就是为什么不允许这种转换。


1
投票
因为

List<PackageCall>

不继承自
List<AbstractPackageCall>
。  您可以使用 
Cast<>()
 扩展方法,例如:

var calls = package.getCalls().Cast<AbstractPackageCall>();
    

0
投票
诀窍是改变这一点:

public abstract class Foo {} public class Bar1: Foo {} public class Bar2: Foo {} public void doSomethingWithFoos(List<Foo> foos) { // ... }
进入这个:

public abstract class Foo {} public class Bar1: Foo {} public class Bar2: Foo {} public void doSomethingWithFoos<T>(List<T> foos) where T: Foo { // ... }
    
© www.soinside.com 2019 - 2024. All rights reserved.