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

247

虽然我们可以继承基类/接口,但为什么不能使用相同的类/接口声明一个List<>

interface A
{ }

class B : A
{ }

class C : B
{ }

class Test
{
    static void Main(string[] args)
    {
        A a = new C(); // OK
        List<A> listOfA = new List<C>(); // compiler Error
    }
}

有什么绕过的方法吗?


1
https://dev59.com/VWQn5IYBdhLWcg3wRVVs - cuongle
13个回答

294

让这个工作的方法是迭代列表并将元素转换。这可以使用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
6
哪一个更快?ConvertAll 还是 Cast - Martin Braun
33
这将创建列表的一个副本。如果您在新列表中添加或删除任何内容,这不会反映在原始列表中。其次,这样做会带来很大的性能和内存损失,因为它会使用现有对象创建一个新列表。请参见下面的答案,以获取没有这些问题的解决方案。 - Bigjim
回答modiX:ConvertAll是List<T>上的方法,因此它只能用于通用列表;它不能用于IEnumerable或IEnumerable<T>;ConvertAll还可以进行自定义转换,而不仅仅是强制转换,例如ConvertAll(inches => inches * 25.4)。Cast<A>是LINQ扩展方法,因此适用于任何IEnumerable<T>(也适用于非泛型IEnumerable),并且像大多数LINQ一样,它使用延迟执行,即仅在检索到多少项时才转换。在此处阅读更多信息:https://codeblog.jonskeet.uk/2011/01/13/reimplementing-linq-to-objects-part-33-cast-and-oftype/ - Edward

193
首先,停止使用无法理解的类名,如A、B、C。使用动物、哺乳动物、长颈鹿或食物、水果、橙子等名称,使关系清晰明了。
你的问题是,“为什么我不能将一组长颈鹿分配给类型为动物列表的变量,因为我可以将长颈鹿分配给类型为动物的变量?”
答案是:假设你可以这样做。那么可能会出什么问题呢?
好吧,你可以将老虎添加到动物列表中。假设我们允许你将一组长颈鹿放入保存动物列表的变量中。然后你尝试将老虎添加到该列表中。会发生什么?你想要长颈鹿列表包含老虎吗?你想要崩溃吗?还是你想让编译器通过使分配在第一次就非法来保护你免受崩溃的影响?
我们选择后者。
这种转换称为“协变”转换。在C# 4中,当已知转换始终安全时,我们将允许您在接口和委托上进行协变转换。有关详细信息,请参见我的关于协变和逆变的博客文章。(本周一和周四都会有关于这个主题的新文章。)

3
如果一个 IList<T> 或 ICollection<T> 实现了非泛型的 IList,但是将 non-generic 的 IList/ICollection 返回 True 作为 IsReadOnly,并对任何可能修改其内容的方法或属性抛出 NotSupportedException 异常,那么这样做会有任何不安全的因素吗? - supercat
54
虽然这个回答包含了相当可接受的推理,但它并不是真正的“正确”。简单的答案是C#不支持这样的操作。拥有一个包含长颈鹿和老虎的动物列表的想法是完全有效的。唯一的问题在于当你想要访问更高级别的类时。实际上这与将父类作为参数传递给函数,然后尝试将其强制转换为不同的子类没有什么区别。可能在实现转换时存在技术问题,但上面的解释并没有提供任何理由说明为什么这会是一个坏主意。 - Paul Coldrey
3
为什么我们可以使用 IEnumerable 而不是 List 进行此转换呢?例如:List<Animal> listAnimals = listGiraffes as List<Animal>; 是不可能的,但是 IEnumerable<Animal> eAnimals = listGiraffes as IEnumerable<Animal> 可以。 - Jéf Bueno
4
请看我的回答的最后一段。那里的转换是已知为安全的。为什么?因为不可能将一系列长颈鹿转换成一系列动物,然后将老虎放入动物序列中IEnumerable<T>IEnumerator<T>都被标记为支持协变,编译器已经验证了这一点。 - Eric Lippert
2
长话短说,这只是一堆苹果,而不是具体的争论。老虎园是动物园的一部分。没有编译器会认为它是错误的。 - IgorZhurbenko
显示剩余3条评论

66

引用Eric的伟大解释:

发生了什么?你想让长颈鹿列表包含一只老虎吗?你想要程序崩溃吗?还是你想要编译器在一开始就保护你免受崩溃的影响,使赋值非法? 我们选择后者。

但是如果你想选择运行时崩溃而不是编译错误怎么办?通常你会使用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视图模型,并得到了漂亮的通用Razor部分视图。太棒了!我很高兴我读到了这个。 - Stefan Cebulak
5
我只会在类声明处添加一个 "where TTo: TFrom",这样编译器就可以警告不正确的用法。制作不相关类型的CastedList毫无意义,而制作 "CastedList<TBase,TDerived>" 也没有用:您不能向其中添加常规的TBase对象,并且您从原始列表中获得的任何TDerived都已经可以用作TBase。 - Wolfzoon
2
@PaulColdrey 唉,这都要怪六年前的先发优势。 - Wolfzoon
@Wolfzoon:标准的.Cast<T>()也没有这个限制。我想让行为与.Cast相同,这样就可以将动物列表转换为老虎列表,并在其中包含长颈鹿时引发异常。就像使用Cast一样... - Bigjim
嗨@Bigjim,我不太明白如何使用你的包装类将现有的长颈鹿数组转换为动物(基类)数组。有什么建议吗?谢谢 :) - Denis Vitez

41
如果您使用IEnumerable,它将起作用(至少在C# 4.0中,我没有尝试过之前的版本)。当然,这只是一个强制转换,它仍然是一个列表。
替换为 -
原始代码中的List<A> listOfA = new List<C>(); // 编译器错误 请改用 - IEnumerable<A> listOfA = new List<C>(); // 编译器错误-不再出现! :)

在那种情况下如何使用IEnumerable? - Vladius
1
在问题的原始代码中,将List<A> listOfA = new List<C>(); // compiler Error替换为IEnumerable<A> listOfA = new List<C>(); // compiler error - no more! :) - PhistucK
1
将IEnumerable<BaseClass>作为参数的方法将允许继承类作为List传递。因此,只需更改参数类型即可完成很少的工作。 - beauXjames
我很好奇为什么当对象在 IEnumerable 中时隐式转换到 A 可行,但当它们在 List 中时却不可行。有人知道原因吗? - Raphael Royer-Rivard

32
关于为什么它不起作用,了解协变和逆变可能会有帮助。
这里是您提供的代码更改示例,以说明为什么它不应该工作:
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>());
}

“这个应该可以工作吗?” - 我会说是和不是。我认为你完全可以编写代码并在编译时失败,因为在访问类型为'C'的FirstItem时实际上正在进行无效转换。有许多类似的方法可以使C#崩溃,而这些方法都得到了支持。_如果_您真的想出于某种好的原因(而且有很多)实现此功能,那么下面bigjim的答案非常棒。 - Paul Coldrey

29

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

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现在有了对象元素 - 这是不应该发生的,并且它确实没有。


2
这个回答对我来说是最易懂的。特别是因为它提到了一个叫做IReadOnlyList的东西,它可以工作,因为它保证不会再添加更多的元素。 - bendtherules
很遗憾,您无法对IReadOnlyDictionary<TKey, TValue>执行类似操作。为什么会这样呢? - Alastair Maw
这是一个很好的建议。我为了方便在任何地方都使用List<>,但大多数情况下我希望它是只读的。这个建议很好,因为它可以通过允许这种转换和改进指定只读的位置来提高我的代码质量。 - Rick Love

4
对于您的问题,有几种C#本地解决方案:
  1. IReadOnlyListIEnumerable => 无误差且类型安全。 这可能是您需要的。
  2. 以正确的方式使用List<> => 无误差/不类型安全
  3. Array => 类型安全/引发运行时错误
  4. dynamicList<object> => 通用/ 不类型安全/ 引发运行时错误

它们都可以很好地工作!没有任何棘手的编程需要!

所有示例的接口和类定义:

using System; using System.Collections.Generic; using System.Linq;

interface IAnimal
{
    public string Name { get; }
}
class Bear : IAnimal
{
    public string BearName = "aBear";
    public string Name => BearName;
}
class Cat : IAnimal
{
    public string CatName = "aCat";
    public string Name => CatName;
}
// Dog has no base class/interface; it isn't related to the other classes
// But it also provides a <Name> property (what a coincidence!)
class Dog
{
    public string DogName = "aDog";
    public string Name => DogName;
}

public class AssignDerivedClass {
    private static string AnyName(dynamic anymal) => anymal switch
    {
        IAnimal animal => animal.Name,
        Dog dog => dog.DogName,
        string s => s,
        _ => "Any unknown Animal"
    };

以下是每个解决方案的示例:
1. IReadOnlyList和IEnumerable:这可能是你需要的
- 将你的List赋值给一个IEnumerable或IReadOnlyList - 两者都不能在运行时更改,即你无法添加或删除元素。 - 你仍然可以修改数据元素本身。 - 使用这个IEnumerable或IReadOnlyList非常便宜,只需通过另一个指针访问数据。 - 但要小心:如果你向IEnumerable或IReadOnlyList附加元素,那么你会创建一个新的IEnumerable。不正确地使用这个IEnumerable可能会变得昂贵。
由于没有元素可以添加,因此所有元素保持正确的类型:
public static void TestIEnumerableAndIReadonlyList()
{
    var cats = new List<Cat>()
    {
        new Cat() { CatName = "Cat-3" },
    };
    IEnumerable<IAnimal> animalsEnumerable = cats;
    IReadOnlyList<IAnimal> animalsReadOnlyList = cats;

    var extendedEnumerable = animalsReadOnlyList.Append(new Bear());
    (extendedEnumerable.First() as Cat).CatName = "Cat-3a";

    Console.WriteLine("Cat names: {0}, {1}, {2}, {3}",
                      cats.ElementAt(0).CatName,
                      animalsReadOnlyList[^1].Name,
                      AnyName(animalsEnumerable.Last()),
                      AnyName(extendedEnumerable.ElementAt(1)));
}
// Result => Cat names: Cat-3a, Cat-3a, Cat-3a, aBear

2. 合理使用 List<>: List<A> listOfA = new()

  • 定义一个接口的List(而不是派生类)
  • 只分配一个派生类的实例 - 你也不想存储其他类,对吗?

将一只猫添加到一个应该是熊的动物列表中:

public static void TestListOfInterface()
{
    var bears = new List<IAnimal>()
    {
        new Bear() { BearName = "Bear-1" },
    };
    bears.Add(new Cat() { CatName = "Cat-2" });

    string bearNames = string.Join(", ", bears.Select(animal => animal.Name));
    Console.WriteLine($"Bear names: {bearNames}");

    static string VerifyBear(IAnimal bear)
        => (bear as Bear)?.Name ?? "disguised as a bear!!!";

    string bearInfo0 = VerifyBear(bears[0]);
    string bearInfo1 = VerifyBear(bears[1]);
    Console.WriteLine($"One animal is {bearInfo0}, the other one is {bearInfo1}");
}
// Bear names: Bear-1, Cat-2
// One animal is Bear-1, the other one is disguised as a bear!!!

3. 数组:创建一个Bear[]数组,保证所有的数组元素都引用了Bear实例。

  • 您可以交换元素,但不能删除或添加新元素。
  • 尝试设置错误的类型会导致运行时错误。

数组中的熊:

public static void TestArray()
{
    Bear[] bears = { new Bear(), null };
    IAnimal[] bearAnimals = bears;

    try { bearAnimals[1] = new Cat(); } // => ArrayTypeMismatchException
    catch (Exception e) { Console.Error.WriteLine(e); } // type incompatible with array

    bearAnimals[1] = new Bear() { BearName = "Bear-2" };
    Console.WriteLine($"Bear names: {bearAnimals[0].Name}, {bears[1].BearName}");
}
// Result => Bear names: aBear, Bear-2

4. dynamicList<dynamic>: 最通用的解决方案

  • 运行时类型检查。
  • 您放弃了编译器的错误检查支持,因此请小心处理!
  • 如果您尝试添加错误类型的元素,则只会获得运行时错误!
  • 如果您访问不存在的成员,则只会获得运行时错误!
  • 您甚至可以分配不相关类的集合。

将列表分配给dynamic或使用List<dynamic>

public static void TestDynamicListAndArray()
{
    dynamic any = new List<Cat>()   // List of specific class - or of interface
    {
        new Cat() { CatName = "Cat-1" },
        new Cat() { CatName = "Cat-2" },
    };
    try { any[0].BearName = "Bear-1"; } // => RuntimeBinderException
    catch (Exception e) { Console.Error.WriteLine(e); } // Cat has no BearName
    try { any.Add(new Bear()); } // => RuntimeBinderException
    catch (Exception e) { Console.Error.WriteLine(e); } // no matching overload

    any[1].CatName += 'a';
    Console.WriteLine($"Animal names: {any[0].CatName}, {any[1].Name}");

    var mix = new List<dynamic>
    {
        new Bear() {BearName = "Bear-3"},
        new Dog() {DogName = "Dog-4"},
        "Cat-5",  // 100MHz ;-)
    };
    Console.WriteLine($"{AnyName(mix[0])}, {mix[1].Name}, {AnyName(mix[2])}");

    try { Console.WriteLine($"Names: {any[2].Name}"); } // => RuntimeBinderException
    catch (Exception e) { Console.Error.WriteLine(e); } // no definition for 'Name'

    any = new Bear() { BearName = "Bear-6" }; // Scalar - not a List or Array!
    try { Console.WriteLine($"{AnyName(any[0])}"); } // => RuntimeBinderException
    catch (Exception e) { Console.Error.WriteLine(e); } // cannot apply indexing []
}
//Animal names: Bear-1, Cat-2a
//Bear-3, Dog-4, Cat-5
} //end of class AssignDerivedClass

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

这是对BigJim的精彩答案的扩展。

在我的情况下,我有一个带有Children字典的NodeBase类,并且我需要一种从子项中通用地进行O(1)查找的方法。我试图在Children的getter中返回一个私有字典字段,因此显然我想避免昂贵的复制/迭代。因此,我使用了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

可能会晚一些。

将其转换为数组也可以完成任务。

main()
{
   List<Camel> camels = new List<Camel>();
   Reproducton(camels.ToArray());
}


public void Reproducton(Animal[] animals)
{
    foreach(var animal in animals.ToList())
    {
       var baby = animal.Reproduce();
    }
}

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