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


Answers:


228

进行此工作的方法是遍历列表并转换元素。这可以使用ConvertAll来完成:

List<A> listOfA = new List<C>().ConvertAll(x => (A)x);

您还可以使用Linq:

List<A> listOfA = new List<C>().Cast<A>().ToList();

2
另一个选项:List <A> listOfA = listOfC.ConvertAll(x =>(A)x);
ahaliav fox 2014年

6
哪一个更快?ConvertAll还是Cast
Martin Braun 2014年

23
这将创建列表的副本。如果您在新列表中添加或删除某些内容,则该内容不会反映在原始列表中。其次,由于它会使用现有对象创建一个新列表,因此会带来很大的性能和内存损失。请参阅下面的答案,以获取没有这些问题的解决方案。
Bigjim 2015年

回答modiX:ConvertAll是List <T>上的一个方法,因此它将仅在通用列表上起作用;它不适用于IEnumerable或IEnumerable <T>; ConvertAll还可以进行自定义转换,而不仅仅是转换,例如ConvertAll(inches => inch * 25.4)。Cast <A>是LINQ扩展方法,因此可在任何IEnumerable <T>上使用(并且也适用于非通用IEnumerable),并且与大多数LINQ一样,它使用延迟执行,即仅转换尽可能多的项检索。在此处了解更多信息:codeblog.jonskeet.uk/2011/01/13/…–
爱德华

171

首先,停止使用无法理解的类名,例如A,B,C。使用Animal,哺乳动物,长颈鹿或Food,Fruit,Orange或关系明确的东西。

那么,您的问题是“为什么不能将长颈鹿列表分配给动物类型列表的变量,因为我可以将长颈鹿分配给动物类型的变量?”

答案是:假设可以。那会出什么问题呢?

好了,您可以将老虎添加到动物列表中。假设我们允许您将长颈鹿列表放入包含动物列表的变量中。然后,您尝试将老虎添加到该列表。怎么了?您是否希望长颈鹿清单中包含老虎?你想撞车吗?还是您想让编译器通过首先将分配设为非法来保护您免受崩溃的影响?

我们选择后者。

这种转换称为“协变”转换。在C#4中,当已知转换始终是安全的时,我们将允许您在接口和委托上进行协变转换。有关详细信息,请参见我的有关协方差和协方差的博客文章。(本周的星期一和星期四将有一个与此主题相关的新主题。)


3
对于实现非通用IList的IList <T>或ICollection <T>,是否会存在任何不安全的问题,但是实现具有非通用Ilist / ICollection的IsReadOnly返回True,并为会对其进行修改的任何方法或属性抛出NotSupportedException?
超级猫

32
尽管此答案包含了可以接受的理由,但它并不是真的“真实”。简单的答案是C#不支持此功能。列出包含长颈鹿和老虎的动物的想法是完全正确的。唯一的问题是当您想访问更高级别的类时。实际上,这与将父类作为参数传递给函数然后尝试将其强制转换为其他同级类没有什么不同。实施强制转换可能存在技术问题,但以上说明并未提供任何理由,这将是一个糟糕的主意。
Paul Coldrey,2016年

2
@EricLippert为什么我们可以使用IEnumerable而不是进行此转换List?即:List<Animal> listAnimals = listGiraffes as List<Animal>;不可能,但是IEnumerable<Animal> eAnimals = listGiraffes as IEnumerable<Animal>有效。
LINQ

3
@jbueno:阅读我的答案的最后一段。那里的转换是安全的。为什么?因为不可能将一系列的长颈鹿变成一系列的动物,然后再将老虎变成一系列的动物IEnumerable<T>IEnumerator<T>都被标记为对协方差是安全的,并且编译器已对此进行了验证。
埃里克·利珀特

60

引用埃里克的出色解释

怎么了?您是否希望长颈鹿清单中包含老虎?你想撞车吗?还是您想让编译器通过首先将分配设为非法来保护您免受崩溃的侵害?我们选择后者。

但是,如果要选择运行时崩溃而不是编译错误怎么办?通常,您将使用Cast <>或ConvertAll <>,但是会遇到两个问题:它将创建列表的副本。如果您在新列表中添加或删除某些内容,则该内容不会反映在原始列表中。其次,由于它会使用现有对象创建一个新列表,因此会带来很大的性能和内存损失。

我遇到了同样的问题,因此我创建了一个包装器类,该类可以转换通用列表而无需创建全新的列表。

在原始问题中,您可以使用:

class Test
{
    static void Main(string[] args)
    {
        A a = new C(); // OK
        IList<A> listOfA = new List<C>().CastList<C,A>(); // now ok!
    }
}

这里是包装器类(+一个易于使用的扩展方法CastList)

public class CastedList<TTo, TFrom> : IList<TTo>
{
    public IList<TFrom> BaseList;

    public CastedList(IList<TFrom> baseList)
    {
        BaseList = baseList;
    }

    // IEnumerable
    IEnumerator IEnumerable.GetEnumerator() { return BaseList.GetEnumerator(); }

    // IEnumerable<>
    public IEnumerator<TTo> GetEnumerator() { return new CastedEnumerator<TTo, TFrom>(BaseList.GetEnumerator()); }

    // ICollection
    public int Count { get { return BaseList.Count; } }
    public bool IsReadOnly { get { return BaseList.IsReadOnly; } }
    public void Add(TTo item) { BaseList.Add((TFrom)(object)item); }
    public void Clear() { BaseList.Clear(); }
    public bool Contains(TTo item) { return BaseList.Contains((TFrom)(object)item); }
    public void CopyTo(TTo[] array, int arrayIndex) { BaseList.CopyTo((TFrom[])(object)array, arrayIndex); }
    public bool Remove(TTo item) { return BaseList.Remove((TFrom)(object)item); }

    // IList
    public TTo this[int index]
    {
        get { return (TTo)(object)BaseList[index]; }
        set { BaseList[index] = (TFrom)(object)value; }
    }

    public int IndexOf(TTo item) { return BaseList.IndexOf((TFrom)(object)item); }
    public void Insert(int index, TTo item) { BaseList.Insert(index, (TFrom)(object)item); }
    public void RemoveAt(int index) { BaseList.RemoveAt(index); }
}

public class CastedEnumerator<TTo, TFrom> : IEnumerator<TTo>
{
    public IEnumerator<TFrom> BaseEnumerator;

    public CastedEnumerator(IEnumerator<TFrom> baseEnumerator)
    {
        BaseEnumerator = baseEnumerator;
    }

    // IDisposable
    public void Dispose() { BaseEnumerator.Dispose(); }

    // IEnumerator
    object IEnumerator.Current { get { return BaseEnumerator.Current; } }
    public bool MoveNext() { return BaseEnumerator.MoveNext(); }
    public void Reset() { BaseEnumerator.Reset(); }

    // IEnumerator<>
    public TTo Current { get { return (TTo)(object)BaseEnumerator.Current; } }
}

public static class ListExtensions
{
    public static IList<TTo> CastList<TFrom, TTo>(this IList<TFrom> list)
    {
        return new CastedList<TTo, TFrom>(list);
    }
}

1
我只是将其用作MVC视图模型,并获得了不错的通用剃刀局部视图。很棒的主意!我很高兴我读了这篇。
Stefan Cebulak

5
我只会在类声明中添加“ where TTo:TFrom”,以便编译器可以警告用法不正确。无论如何,将一个不相关类型的CastedList变成毫无意义,并且使一个“ CastedList <TBase,TDerived>”毫无用处:您不能向其添加常规TBase对象,并且从原始List获得的任何TDerived都可以用作一个TBase。
Wolfzoon

2
@PaulColdrey Alas,要怪六年了。
Wolfzoon

@Wolfzoon:标准.Cast <T>()也没有此限制。我想使行为与.Cast相同,因此可以将“动物”列表转换为“老虎”列表,并且在包含长颈鹿的情况下会有例外。就像Cast ...
Bigjim '17

@Bigjim,您好,我在理解如何使用包装器类,将现有的长颈鹿数组转换为动物(基本类)数组时遇到问题。任何提示将被应用:)
Denis Vitez '18

28

如果您使用 IEnumerable改用它,它将起作用(至少在C#4.0中,我没有尝试过以前的版本)。这只是演员表,当然,它仍然是列表。

代替 -

List<A> listOfA = new List<C>(); // compiler Error

在问题的原始代码中,使用-

IEnumerable<A> listOfA = new List<C>(); // compiler error - no more! :)


在那种情况下如何正确使用IEnumerable?
Vladius

1
而不是List<A> listOfA = new List<C>(); // compiler Error在问题的原始代码中,输入IEnumerable<A> listOfA = new List<C>(); // compiler error - no more! :)
PhistucK

以IEnumerable <BaseClass>作为参数的方法将允许继承的类作为列表传递。因此,除了更改参数类型外,几乎没有其他事情要做。
beauXjames 2015年

27

至于为什么它不起作用,了解协方差和协方差可能会有所帮助

只是为了说明为什么这不应该工作,这里是一个改变你所提供的代码:

void DoesThisWork()
{
     List<C> DerivedList = new List<C>();
     List<A> BaseList = DerivedList;
     BaseList.Add(new B());

     C FirstItem = DerivedList.First();
}

应该行吗?列表中的第一项的类型为“ B”,但DerivedList项的类型为C。

现在,假设我们真的只是想做一个通用函数,该函数在实现A的某种类型的列表上操作,但是我们不在乎是什么类型:

void ThisWorks<T>(List<T> GenericList) where T:A
{

}

void Test()
{
     ThisWorks(new List<B>());
     ThisWorks(new List<C>());
}

“这行得通吗?” -我会的,不会的。我认为没有理由为什么您不能编写代码并使它在编译时失败,因为在您访问FirstItem为'C'类型时,它实际上在进行了无效的转换。有许多类似的方法可以炸毁C#。如果您确实有充分的理由(并且有很多原因)想要实现此功能,那么bigjim在下面的回答就很棒。
Paul Coldrey '16

16

您只能转换为只读列表。例如:

IEnumerable<A> enumOfA = new List<C>();//This works
IReadOnlyCollection<A> ro_colOfA = new List<C>();//This works
IReadOnlyList<A> ro_listOfA = new List<C>();//This works

而且,对于支持保存元素的列表,您不能这样做。原因是:

List<string> listString=new List<string>();
List<object> listObject=(List<object>)listString;//Assume that this is possible
listObject.Add(new object());

现在怎么办?请记住,listObject和listString实际上是相同的列表,因此listString现在具有object元素-应该是不可能的,而不可能。


1
这是我最容易理解的答案。特别是因为它提到有一个叫做IReadOnlyList的东西,它可以工作,因为它保证不会再添加任何元素。
benttherules 17-10-14

遗憾的是,您无法为IReadOnlyDictionary <TKey,TValue>做类似的事情。这是为什么?
Alastair Maw

这是一个很好的建议。为了方便起见,我在各处都使用List <>,但是大多数时候我希望它是只读的。不错的建议,因为这将通过允许这种类型的转换并改进我指定只读的位置以两种方式改进我的代码。
里克·洛夫

1

我个人喜欢创建带有扩展类的库

public static List<TTo> Cast<TFrom, TTo>(List<TFrom> fromlist)
  where TFrom : class 
  where TTo : class
{
  return fromlist.ConvertAll(x => x as TTo);
}

0

因为C#不允许这种类型的 遗产此刻转换。


6
首先,这是可转换性的问题,而不是继承性的问题。其次,泛型类型的协方差不适用于类类型,仅适用于接口和委托类型。
埃里克·利珀特

5
好吧,我很难和你争论。
中午丝绸

0

这是BigJim出色答案的扩展。

在我的情况下,我有一NodeBase类带有Children字典的类,并且我需要一种从子类进行O(1)查找的方法。我试图在的getter中返回私有字典字段Children,因此显然我想避免昂贵的复制/迭代。因此,我使用Bigjim的代码将强制Dictionary<whatever specific type>转换为泛型Dictionary<NodeBase>

// Abstract parent class
public abstract class NodeBase
{
    public abstract IDictionary<string, NodeBase> Children { get; }
    ...
}

// Implementing child class
public class RealNode : NodeBase
{
    private Dictionary<string, RealNode> containedNodes;

    public override IDictionary<string, NodeBase> Children
    {
        // Using a modification of Bigjim's code to cast the Dictionary:
        return new IDictionary<string, NodeBase>().CastDictionary<string, RealNode, NodeBase>();
    }
    ...
}

这很好。但是,我最终遇到了不相关的限制,最终FindChild()在基类中创建了一个抽象方法来代替查找。事实证明,这首先消除了对强制转换字典的需求。(IEnumerable出于我的目的,我能够用一个简单的替换它。)

因此,您可能会问的问题(尤其是性能是否是禁止您使用.Cast<>或的问题.ConvertAll<>)是:

“我真的需要转换整个集合吗,还是可以使用抽象方法来保存执行任务所需的特殊知识,从而避免直接访问集合?”

有时最简单的解决方案是最好的。


0

您还可以使用System.Runtime.CompilerServices.UnsafeNuGet包创建对相同对象的引用List

using System.Runtime.CompilerServices;
...
class Tool { }
class Hammer : Tool { }
...
var hammers = new List<Hammer>();
...
var tools = Unsafe.As<List<Tool>>(hammers);

给定上面的示例,您可以Hammer使用tools变量访问列表中的现有实例。将Tool实例添加到列表会引发ArrayTypeMismatchException异常,因为tools引用的变量与相同hammers


0

我已经阅读了整个主题,我只想指出对我来说似乎是矛盾的。

编译器阻止您使用列表进行分配:

List<Tiger> myTigersList = new List<Tiger>() { new Tiger(), new Tiger(), new Tiger() };
List<Animal> myAnimalsList = myTigersList;    // Compiler error

但是编译器可以很好地使用数组:

Tiger[] myTigersArray = new Tiger[3] { new Tiger(), new Tiger(), new Tiger() };
Animal[] myAnimalsArray = myTigersArray;    // No problem

关于分配是否已知是安全的争论在这里分开了。我对数组所做的分配并不安全。为了证明这一点,如果我对此进行跟进:

myAnimalsArray[1] = new Giraffe();

我得到一个运行时异常 “ ArrayTypeMismatchException”。如何解释这一点?如果编译器确实希望阻止我执行某些愚蠢的操作,则它应该阻止了我执行数组分配。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.