无法将List<DerivedClass>转换为List<BaseClass>

60

我正在尝试将一个 DerivedClass 列表传递给一个接受 BaseClass 列表的函数,但是我遇到了错误:

cannot convert from 
'System.Collections.Generic.List<ConsoleApplication1.DerivedClass>' 
to 
'System.Collections.Generic.List<ConsoleApplication1.BaseClass>'

现在我可以将我的List<DerivedClass>转换为List<BaseClass>,但除非我理解编译器为什么不允许这样做,否则我不感到舒服。

我找到的解释只��说它违反了类型安全性,但我没有看出来。有人能帮我吗?

如果编译器允许从List<DerivedClass>转换为List<BaseClass>,会有什么风险?


这是我的SSCCE:

class Program
{
    public static void Main()
    {
        BaseClass bc = new DerivedClass(); // works fine
        List<BaseClass> bcl = new List<DerivedClass>(); // this line has an error

        doSomething(new List<DerivedClass>()); // this line has an error
    }

    public void doSomething(List<BaseClass> bc)
    {
        // do something with bc
    }
}

class BaseClass
{
}

class DerivedClass : BaseClass
{
}

该类继承自其基类,但List<Derived>并不继承自List<Base> - Tim Schmelter
@TimSchmelter这是否意味着这种继承可能成为一项功能,其中Generic<Derived>可以被认为是从Generic<Base>派生而来?或者这样的功能是否会引起问题? - Sam I am says Reinstate Monica
1
它只是没有以那种方式实现。http://marcgravell.blogspot.fr/2009/02/what-c-40-covariance-doesn-do.html - Tim Schmelter
@TimSchmelter:不行,这样是不可能的。 - SLaks
1
@SamIam:你应该一开始就实例化它为 List<Base> - voithos
显示剩余2条评论
6个回答

81

之所以要更改为IEnumerable<T>,是因为List<T>不变的,而不是协变的IEnumerable<T>支持协变。

IEnumerable<BaseClass> bcl = new List<DerivedClass>();

public void doSomething(IEnumerable<BaseClass> bc)
{
    // do something with bc
}

有关泛型中的协变的信息。


4
从.NET 4.5开始,如果方法需要对集合元素进行随机访问,则还可以使用IReadOnlyList<T> - tia

63
我发现的解释只说它违反了类型安全,但我不太理解。允许将List<DerivedClass>转换为List<BaseClass>有什么风险?
这个问题几乎每天都会被问到。
因为你可以把蜥蜴放进动物列表中,所以List<Mammal>不能转换为List<Animal>List<Mammal>也不能转换为List<Giraffe>,因为列表中已经有老虎了。
因此List<T>必须在T方面是不变的。
然而,List<Mammal>可以转换为IEnumerable<Animal>(从C# 4.0开始),因为IEnumerable<Animal>上没有添加蜥蜴的方法。IEnumerable<T>在T方面是协变的。

6
一个简单的动物学解释可以概括所有问题。(IEnumerable<Animal>上没有添加蜥蜴的方法 --> 我会再检查一遍;) - vc 74
4
我感到有点蠢...长颈鹿的问题似乎很明显,但为什么你不能将哺乳动物传递给能够处理动物的东西,只是因为它也可以处理非哺乳动物? - MGOwen
6
你有一份哺乳动物清单。你决定把它当成动物清单来处理,并将一只蜥蜴放入动物清单中。但实际上,这是一份哺乳动物清单。现在你手上有一份既包含哺乳动物又包含蜥蜴的清单,违反了哺乳动物清单应该只包含哺乳动物的期望。结论是类型系统的违规,因此其中一步必须是不合法的。而第一步就是不合法的:你不能把哺乳动物清单当成动物清单来处理。 - Eric Lippert
3
@RyanPergent: 这不是一个新列表,而是同一个列表。List<T>是一种“引用类型”,引用转换会“重新解释”引用的含义,但它不会改变引用所指向的对象。 - Eric Lippert
3
@RyanPergent: mammals.Cast<Animal>().ToList() 将会创建一个新的 List<Animal>,其内容与 mammals 相同,但该转换需要构建列表的副本,因此速度较慢。如果优化器聪明,引用转换可能根本不需要时间,因为引用转换除了如何解释引用之外,什么也不改变 - Eric Lippert
显示剩余4条评论

16
您所描述的行为称为协变 - 如果A B,那么List<A>List<B>
但是,对于可变类型(如List<T>),这在根本上是不安全的。
如果这种情况成立,那么该方法就能向一个实际只能容纳DerivedClass的列表中添加一个new OtherDerivedClass()
协变在不可变类型上是安全的,尽管.NET仅在接口和委托中支持它。如果您将List<T>参数更改为IEnumerable<T>,那么它就可以工作了。

@SamIam:不行;你试图传递一个 List<Derived>。传递一个包含 DerivedList<Base> 就可以正常工作。 - SLaks
那么,如果我在传递之前将我的 List<Derived> 强制转换或转换为 List<Base>,这样做可以吗? - Sam I am says Reinstate Monica
1
@SamIam:不行。整个问题在于将List<Derived>强制转换为List<Base>基本上是不安全的。如果一开始就创建一个List<Base>,它会正常工作。 - SLaks
@SamIam:SLaks 已经讲得很清楚了。如果你可以把一个 List<Derived> 转换成 List<Base>,那么你理论上就可以向其中添加一个 OtherDerived 的实例(因为这个列表是 可变 的)。但是它被实例化为 List<Derived>,如果你试图手动将其强制转换回 List<Derived>,你会得到一个包含 OtherDerivedList<Derived> - voithos

2

当你有一个从基类派生的类时,这些类的任何容器都不会自动派生。因此,你不能将 List<Derived> 强制转换为 List<Base>

使用 .Cast<T>() 创建一个新列表,在该列表中将每个对象强制转换回基类:

List<MyDerived> list1 = new List<MyDerived>();
List<MyBase> list2 = list1.Cast<MyBase>().ToList();

请注意,这是一个新列表,而不是您原始列表的拷贝版本,因此对这个新列表的操作将不会反映在原始列表上。但是,对于列表中包含的对象的操作将反映出来。

3
我认为使用 List<MyBase> list2 = new List<MyBase>(list1); 比使用 Cast 更加简洁。 - Brian

1
如果你能够写出:
List<BaseClass> bcl = new List<DerivedClass>(); 

你可以称之为
var instance = new AnotherClassInheritingFromBaseClass();
bc1.Add(instance);

将一个不是DerivedClass的实例添加到列表中。

0
我使用的解决方案是创建一个扩展类:
public static class myExtensionClass 
{
    public void doSomething<T>(List<BaseClass> bc) where T: BaseClass
    {
        // do something with bc
    }
}

它使用泛型,但当您调用它时,您不必指定类,因为您已经“告诉”编译器类型与扩展类相同。

您可以这样调用它:

List<DerivedClass> lst = new List<DerivedClass>();
lst.doSomething();

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