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

Asa*_*sad 162 c# collections inheritance list covariance

虽然我们可以从基类/接口继承,但为什么我们不能声明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
    }
}
Run Code Online (Sandbox Code Playgroud)

有办法吗?

Mar*_*ers 213

使这项工作的方法是迭代列表并转换元素.这可以使用ConvertAll完成:

List<A> listOfA = new List<C>().ConvertAll(x => (A)x);
Run Code Online (Sandbox Code Playgroud)

你也可以使用Linq:

List<A> listOfA = new List<C>().Cast<A>().ToList();
Run Code Online (Sandbox Code Playgroud)

  • 这将创建列表的副本.如果在新列表中添加或删除某些内容,则不会在原始列表中反映出来.其次,由于它使用现有对象创建新列表,因此存在很大的性能和内存损失.请参阅下面的答案,了解没有这些问题的解决方案 (16认同)
  • 哪一个更快?`ConvertAll`或`Cast`? (6认同)
  • 另一种选择:List <A> listOfA = listOfC.ConvertAll(x =>(A)x); (2认同)

Eri*_*ert 156

首先,停止使用不可理解的类名,如A,B,C.使用动物,哺乳动物,长颈鹿或食物,水果,橙或其他关系清晰的东西.

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

答案是:假设你可以.什么可能会出错?

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

我们选择后者.

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

  • 虽然这个答案包含了完全可以接受的推理,但它并不是真的"真实".简单的答案是C#不支持这一点.拥有包含长颈鹿和老虎的动物清单的想法是完全有效的.当您想要访问更高级别的类时,唯一的问题就出现了.实际上,将父类作为参数传递给函数然后尝试将其转换为不同的兄弟类也没有什么不同.实施演员表可能存在技术问题,但上面的解释并没有提供任何理由说明这是一个坏主意. (25认同)
  • 对于实现非泛型IList的IList <T>或ICollection <T>是否存在任何不安全因素,但实现非通用Ilist/ICollection为IsReadOnly返回True,并对任何会修改它的方法或属性抛出NotSupportedException? (3认同)
  • @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;` 是可行的。 (3认同)
  • @jbueno:阅读我的答案的最后一段。那里的转换*是安全的*。为什么?因为不可能将长颈鹿序列变成动物序列,然后“将老虎变成动物序列” *。IEnumerable &lt;T&gt;和IEnumerator &lt;T&gt;都被标记为协方差安全的,编译器已经验证了这一点。 (2认同)
  • 很长,但没有真正的论点。一串苹果就是一串水果。老虎动物园是动物的动物园。并且没有编译器可以使它不正确。 (2认同)
  • @DomenPigeon:好问题。是的,不可变的苹果数组可以安全地用作不可变的水果数组。这正是 IEnumerable 可以协变的原因;界面中没有“改变此成员”功能。不幸的是,我们无法用类型来表达“实现是不可变的”。 (2认同)

Big*_*jim 56

引用埃里克的伟大解释

怎么了?你想要长颈鹿的名单包含一只老虎吗?你想要崩溃吗?或者您是否希望编译器通过首先使分配非法来保护您免受崩溃?我们选择后者.

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

我遇到了同样的问题,因此我创建了一个包装器类,可以在不创建全新列表的情况下转换通用列表.

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

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

这里是包装类(+一个扩展方法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);
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 我只会在类声明中添加"where TTo:TFrom",因此编译器可以警告不正确的用法.制作不相关类型的CastedList是无意义的,并且制作"CastedList <TBase,TDerived>"将是无用的:您不能向其添加常规TBase对象,并且您从原始List获得的任何TDerived都可以用作一个TBase. (5认同)
  • @PaulColdrey唉,六年的首发应该受到责备. (2认同)

Chr*_*man 24

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

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

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

     C FirstItem = DerivedList.First();
}
Run Code Online (Sandbox Code Playgroud)

这有用吗?列表中的第一项是"B"类型,但DerivedList项的类型是C.

现在,假设我们真的只想创建一个泛型函数,该函数在某个实现A的类型的列表上运行,但我们不关心它是什么类型:

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

}

void Test()
{
     ThisWorks(new List<B>());
     ThisWorks(new List<C>());
}
Run Code Online (Sandbox Code Playgroud)


Phi*_*ucK 22

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

代替 -

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

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

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


小智 15

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

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
Run Code Online (Sandbox Code Playgroud)

并且您不能为支持保存元素的列表执行此操作.原因是:

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

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

  • 这对我来说是最容易理解的答案。特别是因为它提到有一个叫做 IReadOnlyList 的东西,它会起作用,因为它保证不会添加更多元素。 (2认同)

uwo*_*ose 5

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

  1. IReadOnlyList, IEnumerable=>无错误且类型安全。 你可能需要什么。
  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"
    };
Run Code Online (Sandbox Code Playgroud)

以下是每个解决方案的示例:

1. IReadOnlyList, IEnumerable: 您可能需要什么

  • 将您分配List<C>IEnumerable<A>IReadOnlyList<A>
  • 它们都不能在运行时更改,即您不能AddRemove元素。
  • 您仍然可以自行修改数据元素。
  • 用这个IEnumerable还是IReadOnlyList很便宜的。只需通过另一个指针访问您的数据。
  • 但请注意:如果您将Append元素添加到 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
Run Code Online (Sandbox Code Playgroud)

2、正确使用List<>方法: List<A> listOfA = new()

  • 定义接口的列表(不是派生类的)
  • 仅分配一个派生类的实例 - 无论如何您都不想存储其他类,是吗?

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

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!!!
Run Code Online (Sandbox Code Playgroud)

3. Array:创建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
Run Code Online (Sandbox Code Playgroud)

4. dynamicand List<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
Run Code Online (Sandbox Code Playgroud)