218

虽然我们可以从基类/接口继承,但为什么我们不能声明一个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
    }
}

有办法吗?

4

12 回答 12

274

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

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

你也可以使用 Linq:

List<A> listOfA = new List<C>().Cast<A>().ToList();
于 2009-11-30T00:43:18.467 回答
181

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

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

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

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

我们选择后者。

这种转换称为“协变”转换。在 C# 4 中,当已知转换始终是安全的时,我们将允许您对接口和委托进行协变转换。有关详细信息,请参阅我关于协变和逆变的博客文章。(这周的周一和周四都会有关于这个话题的新话题。)

于 2009-11-30T00:45:56.560 回答
66

引用埃里克的伟大解释

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

但是,如果您想选择运行时崩溃而不是编译错误怎么办?你通常会使用 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);
    }
}
于 2015-06-05T09:01:35.447 回答
34

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

代替 -

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

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

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

于 2015-02-25T14:32:30.550 回答
31

至于为什么它不起作用,理解covariance 和 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>());
}
于 2009-11-30T00:55:20.777 回答
21

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

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

于 2017-03-16T12:52:51.957 回答
2

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

  1. dynamic

  2. Array

  3. IReadOnlyList,IEnumerable

  4. 使用List<>正确的方法。

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

以下是每个示例:

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

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

简单地写dynamic listOfA = new List<C>();而不是List<A> listOfA = new List<C>();

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

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. IReadOnlyList,IEnumerable :

  • 将您分配List<C>给一个IEnumerable<A>IReadOnlyList<A>

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

  • 为什么编译器应该允许将您分配List<C>给 aList<A>而不是IReadOnlyList<A>在添加元素时会导致错误?

      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<>正确的方法: List<A> listOfA = new List<A>()

  • 定义你的接口列表

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

      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!!!
    
于 2021-03-30T12:37:19.143 回答
1

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

public static List<TTo> Cast<TFrom, TTo>(List<TFrom> fromlist)
  where TFrom : class 
  where TTo : class
{
  return fromlist.ConvertAll(x => x as TTo);
}
于 2018-04-21T15:55:28.283 回答
0

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

于 2009-11-30T00:32:01.023 回答
0

这是 BigJim 出色答案的扩展。

就我而言,我有一个NodeBaseChildren字典的类,我需要一种方法来一般地从孩子那里进行 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<>or的问题.ConvertAll<>)是:

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

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

于 2016-05-04T01:32:54.133 回答
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列表中的现有实例。toolsTool实例添加到列表会引发ArrayTypeMismatchException异常,因为tools引用的变量与hammers.

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

于 2020-04-02T16:36:52.627 回答