为什么我不能将List <derived>分配给List <base />?</derived>

时间:2011-01-10 23:52:38

标签: c# .net generics covariance

我定义了以下类:

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;
        }

5 个答案:

答案 0 :(得分:54)

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

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必须对每个数组插入进行类型检查,如果它实际上不是{{1}则抛出异常}}。这可能是一个(小)性能损失,但这可以通过在任一类型周围创建Apple包装器来规避,因为这种检查不会发生在值类型中(因为它们不能从任何东西继承)。起初,我不明白为什么做出这个决定,但你会经常遇到为什么这个有用。最常见的是struct,它取String.Format并且任何数组都可以传递给它。

在.NET 4中,有类型安全的协方差/逆变,它允许你做出一些像这样的任务,但前提是它们是可证明安全的。什么是可靠的安全?

params object[]

以上在.NET 4中有效,因为IEnumerable<Fruit> fruits = new List<Apple>(); 变为IEnumerable<T>IEnumerable<out T>表示out T只能 fruits,而IEnumerable<out T>上的根本没有方法 } T作为参数,因此您永远不能错误地将Banana 传递到 IEnumerable<Fruit>

逆变法大致相同,但我总是忘记它的具体细节。不出所料,为此,类型参数现在有in个关键字。

答案 1 :(得分: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}}。

答案 2 :(得分:2)

现在,如果您想进行以下呼叫:

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;
    }
}

答案 3 :(得分:1)

因为List<PackageCall>不会从List<AbstractPackageCall>继承。您可以使用Cast<>()扩展名方法,例如:

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

答案 4 :(得分:0)

您无法执行此转换,因为List<AbstractPackageCall>可以包含从AbstractPackageCall派生的任何内容,而List<PackageCall>则不能 - 它只能包含从{{1}派生的内容}。

考虑是否允许此演员表:

PackageCall

您刚刚将class AnotherPackageCall : AbstractPackageCall { } // ... List<AbstractPackageCall> calls = package.getCalls(); calls.Add(new AnotherPackageCall()); 添加到AnotherPackageCall - 但List<PackageCall>并非来自AnotherPackageCall!这就是为什么不允许这种转换。