【问题标题】:Convert List<DerivedClass> to List<BaseClass>将 List<DerivedClass> 转换为 List<BaseClass>
【发布时间】:2010-12-21 11:57:18
【问题描述】:

虽然我们可以从基类/接口继承,但为什么我们不能声明 List&lt;&gt; 使用相同的类/接口?

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

有办法吗?

【问题讨论】:

标签: c# list inheritance collections covariance


【解决方案1】:

对于您的问题,有几种 native C# 可能性:

  1. dynamic

  2. Array

  3. IReadOnlyList, IEnumerable

  4. 正确使用List&lt;&gt;

    它们都运行良好!不需要任何棘手的编程!

以下是每个示例:

1. dynamic:最通用的解决方案

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

只需写dynamic listOfA = new List&lt;C&gt;(); 而不是List&lt;A&gt; listOfA = new List&lt;C&gt;();

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

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
class Dog
{
    public string DogName = "aDog";
    public string Name => DogName;
}

这是使用dynamic的示例

public class AssignDerivedClass
{
    public static void TestDynamicListAndArray()
    {
        dynamic any = new List<Bear>()   // List of derived
        {
            new Bear() { BearName = "Bear-1" },
            new Bear() { BearName = "Bear-2" }
        };
        //any[0].CatName = "NewCat"; // => Microsoft.CSharp.RuntimeBinder.RuntimeBinderException
        Console.WriteLine($"Bear names: {any[0].BearName}, {Name(any[1])}");

        any = new Cat[]   // Array of derived
        {
            new Cat() { CatName = "Cat-3" },
            new Cat() { CatName = "Cat-4" }
        };
        Console.WriteLine($"Cat names: {any[0].CatName}, {any[1].Name}");

        any = new List<Dog>()   // List of non-related class
        {
            new Dog() { DogName = "Dog-5" },
            new Dog() { DogName = "Dog-6" }
        };
        Console.WriteLine($"Dog names: {any[0].DogName}, {Name(any[1])}");

        any = new List<IAnimal>()   // List of interface
        // any = new IAnimal[]   // Array of interface works the same
        {
            new Bear() { BearName = "Bear-7" },
            new Cat() { CatName = "Cat-8" }
        };
        Console.WriteLine($"Animal names: {any[0].BearName}, {any[1].CatName}");

        any[0].BearName = "NewBear";
        Console.WriteLine($"Animal names: {Name(any[0])}, {any[1].Name}");
    }

    private static string Name(dynamic anymal)
    {
        return anymal switch
        {
            Bear bear => bear.BearName,
            Cat cat => cat.CatName,
            Dog dog => dog.DogName,
            _ => "No known Animal"
        };
    }
    // Bear names: Bear-1, Bear-2
    // Cat names: Cat-3, Cat-4
    // Dog names: Dog-5, Dog-6
    // Animal names: Bear-7, Cat-8
    // Animal names: NewBear, Cat-8
}

2. Array:创建Bear[]数组,保证所有数组元素引用Bear的实例。

  • 您可以交换元素,但不能删除或添加新元素。

  • 尝试设置错误的类型会产生运行时错误。

      public static void TestArray()
      {
          Bear[] bears = { new Bear(), null };
          IAnimal[] bearAnimals = bears;
    
          //bearAnimals[1] = new Cat(); // System.ArrayTypeMismatchException
          bearAnimals[1] = new Bear() { BearName = "Bear-1" };
          Console.WriteLine($"Bear names: {bearAnimals[0].Name}, {bears[1].BearName}");
      }
      // Result => Bear names: aBear, Bear-1
    

3。 IReadOnlyListIEnumerable

  • 将您的List&lt;C&gt; 分配给IEnumerable&lt;A&gt;IReadOnlyList&lt;A&gt;

  • 它们都不能在运行时更改,即您不能添加或删除元素。

  • 当添加元素会导致错误时,为什么编译器应该允许将您的List&lt;C&gt; 分配给List&lt;A&gt; 而不是IReadOnlyList&lt;A&gt;

      public static void TestIEnumerableAndIReadonlyList()
      {
          var cats = new List<Cat>()
          {
              new Cat() { CatName = "Cat-3" },
              new Cat() { CatName = "Cat-4" }
          };
          IEnumerable<IAnimal> iEnumerable = cats;
          Console.WriteLine($"Cat names: {(iEnumerable.ElementAt(0) as Cat).CatName}, "
              + Name(iEnumerable.Last()));
    
          IReadOnlyList<IAnimal> iROList = cats;
          Console.WriteLine($"Cat names: {iROList[0].Name}, {Name(iROList[1])}");
    
          //iROList.Add(new Cat()); // compiler error CS61: no definition for 'Add'
      }
      // Result:
      // Cat names: Cat-3, Cat-4
      // Cat names: Cat-3, Cat-4
    

4。正确使用List&lt;&gt; List&lt;A&gt; listOfA = new List&lt;A&gt;()

  • 定义你的界面列表

  • 只分配一个派生类的实例 - 你不想存储其他类,是吗?

      public static void TestListOfInterface()
      {
          var bears = new List<IAnimal>()
          {
              new Bear() { BearName = "Bear-1" },
              new Cat() { CatName = "Cat-3" },
          };
          bears.Add(new Bear() { BearName = "Bear-2" });
    
          string bearNames = string.Join(", ", bears.Select(animal => animal.Name));
          Console.WriteLine($"Bear names: {bearNames}");
    
          string bearInfo0 = VerifyBear(bears[0]);
          string bearInfo1 = VerifyBear(bears[1]);
          Console.WriteLine($"One animal is {bearInfo0}, the other one is {bearInfo1}");
    
          string VerifyBear(IAnimal bear)
              => (bear as Bear)?.BearName ?? "disguised as a bear!!!";
      }
      // Bear names: Bear-1, Cat-3, Bear-2
      // One animal is Bear-1, the other one is disguised as a bear!!!
    

【讨论】:

    【解决方案2】:

    我已经阅读了整篇文章,我只想指出对我来说似乎不一致的地方。

    编译器会阻止您使用列表进行赋值:

    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”。如何解释这一点?如果编译器真的想阻止我做一些愚蠢的事情,它应该阻止我做数组赋值。

    【讨论】:

      【解决方案3】:

      您还可以使用System.Runtime.CompilerServices.Unsafe NuGet 包创建对相同List 的引用:

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

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

      【讨论】:

        【解决方案4】:

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

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

        【讨论】:

          【解决方案5】:

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

          而不是-

          List&lt;A&gt; listOfA = new List&lt;C&gt;(); // compiler Error

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

          IEnumerable&lt;A&gt; listOfA = new List&lt;C&gt;(); // compiler error - no more! :)

          【讨论】:

          • 在这种情况下如何使用 IEnumerable?
          • 问题原代码中的List&lt;A&gt; listOfA = new List&lt;C&gt;(); // compiler Error,输入IEnumerable&lt;A&gt; listOfA = new List&lt;C&gt;(); // compiler error - no more! :)
          • 以 IEnumerable 作为参数的方法将允许继承的类作为列表传入。因此,除了更改参数类型外,几乎没有什么可做的。
          【解决方案6】:

          引用埃里克的精彩解释

          会发生什么?您希望长颈鹿列表中包含一只老虎吗?你想崩溃吗?还是您希望编译器首先通过使分配非法来保护您免受崩溃? 我们选择后者。

          但是,如果您想选择运行时崩溃而不是编译错误怎么办?你通常会使用 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);
              }
          }
          

          【讨论】:

          • 我刚刚将它用作 MVC 视图模型并获得了不错的通用剃须刀局部视图。惊人的想法!我很高兴能读到这篇文章。
          • 我只会在类声明中添加一个“where TTo : TFrom”,这样编译器就可以警告不正确的使用。无论如何,制作一个不相关类型的 CastedList 是无意义的,制作一个“CastedList”将是无用的:您不能向其中添加常规 TBase 对象,并且您从原始 List 获得的任何 TDerived 都可以用作一个 TBase。
          • @PaulColdrey 唉,六年的领先是罪魁祸首。
          • @Wolfzoon:标准的 .Cast() 也没有这个限制。我想让行为与 .Cast 相同,因此可以将 Animals 列表转换为 Tigers 列表,并且当它包含 Giraffe 时有一个例外。就像 Cast 一样...
          • 嗨@Bigjim,我在理解如何使用您的包装类将现有的长颈鹿数组转换为动物数组(基类)时遇到问题。任何提示将被应用:)
          【解决方案7】:

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

          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 元素 - 这应该是不可能的,但事实并非如此。

          【讨论】:

          • 这是我最容易理解的答案。特别是因为它提到有一个叫做 IReadOnlyList 的东西,它会起作用,因为它保证不会添加更多元素。
          • 很遗憾你不能为 IReadOnlyDictionary 做类似的事情。这是为什么呢?
          • 这是一个很好的建议。为了方便起见,我到处使用 List,但大多数时候我希望它是只读的。很好的建议,因为这将通过允许这种强制转换并改进我指定只读的位置以两种方式改进我的代码。
          【解决方案8】:

          这是 BigJim 出色的 answer 的扩展。

          在我的例子中,我有一个带有 Children 字典的 NodeBase 类,我需要一种方法来一般地从孩子那里进行 O(1) 查找。我试图在 Children 的 getter 中返回一个私有字典字段,所以显然我想避免昂贵的复制/迭代。因此,我使用 Bigjim 的代码将 Dictionary&lt;whatever specific type&gt; 转换为通用 Dictionary&lt;NodeBase&gt;

          // 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&lt;&gt;.ConvertAll&lt;&gt; 的问题)是:

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

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

          【讨论】:

            【解决方案9】:

            至于为什么不起作用,了解covariance and contravariance可能会有所帮助。

            只是为了说明为什么这个不应该工作,这里是对您提供的代码的更改:

            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 的答案很棒。
            【解决方案10】:

            因为 C# 不允许那种类型的 inheritance 转换at the moment

            【讨论】:

            • 首先,这是一个可转换性的问题,而不是继承的问题。其次,泛型类型的协变不适用于类类型,仅适用于接口和委托类型。
            • 好吧,我很难和你争论。
            【解决方案11】:

            首先,停止使用难以理解的类名,如 A、B、C。使用 Animal、Mammal、Giraffe 或 Food、Fruit、Orange 或关系明确的东西。

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

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

            好吧,您可以将老虎添加到动物列表中。假设我们允许您将长颈鹿列表放入包含动物列表的变量中。然后您尝试将老虎添加到该列表中。发生什么了?您希望长颈鹿列表中包含一只老虎吗?你想崩溃吗?还是您希望编译器首先通过使赋值非法来保护您免受崩溃?

            我们选择后者。

            这种转换称为“协变”转换。在 C# 4 中,我们将允许您在接口和委托上进行协变转换当转换被认为总是安全的时。有关详细信息,请参阅我关于协变和逆变的博客文章。 (本周星期一和星期四都会有关于这个主题的新内容。)

            【讨论】:

            • 实现非泛型 IList 的 IList 或 ICollection 是否存在任何不安全之处,但实现非泛型 Ilist/ICollection 为 IsReadOnly 返回 True,并为任何类型抛出 NotSupportedException会修改它的方法或属性?
            • 虽然这个答案包含完全可以接受的推理,但它并不是真正的“真实”。简单的答案是 C# 不支持这一点。列出包含长颈鹿和老虎的动物列表的想法是完全有效的。当您想要访问更高级别的类时,唯一的问题就出现了。实际上,这与将父类作为参数传递给函数然后尝试将其强制转换为不同的兄弟类没有什么不同。实施演员表可能存在技术问题,但上述解释并未提供任何理由说明这是一个坏主意。
            • @EricLippert 为什么我们可以使用IEnumerable 而不是List 进行这种转换?即:List&lt;Animal&gt; listAnimals = listGiraffes as List&lt;Animal&gt;; 是不可能的,但 IEnumerable&lt;Animal&gt; eAnimals = listGiraffes as IEnumerable&lt;Animal&gt; 有效。
            • @jbueno:阅读我回答的最后一段。那里的转换已知是安全的。为什么?因为不可能把一个长颈鹿序列变成一个动物序列,然后把一只老虎放进动物序列IEnumerable&lt;T&gt;IEnumerator&lt;T&gt; 都被标记为对协方差安全,并且编译器已经验证了这一点。
            • 很长,但没有真正的论据。一串苹果就是一串水果。老虎动物园是动物的动物园。并且没有编译器可以使它不正确。
            【解决方案12】:

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

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

            你也可以使用 Linq:

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

            【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2013-06-02
            相关资源
            最近更新 更多