Convert List<DerivedClass> to List<BaseClass>

Viewed 136046

While we can inherit from base class/interface, why can't we declare a List<> using same class/interface?

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

Is there a way around?

12 Answers

For your problem there are several native C# possibilities:

  1. dynamic

  2. Array

  3. IReadOnlyList, IEnumerable

  4. Use List<> the proper way.

    All of them work well! There is no need for any tricky programming!

Here are examples for each of them:

1. dynamic: The most universal solution

  • type checking at runtime
  • you abandon your compiler error checking support, so handle with care! If you try to add an element of wrong type, you'll only get a runtime error!
  • you even can assign collections of unrelated classes.

Simply write dynamic listOfA = new List<C>(); instead of List<A> listOfA = new List<C>();

At first the interface and class definitions for all of the examples:

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

Here is the example using 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: Creating a Bear[] array, it is guaranteed that all array elements reference instances of Bear.

  • You can exchange elements, but you can't remove or add new elements.

  • Trying to set a wrong type yields a runtime error.

      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:

  • Assign your List<C> to an IEnumerable<A> or IReadOnlyList<A>

  • Neither of them can be changed at runtime, i.e. you can't Add or Remove elements.

  • Why should the compiler allow assigning your List<C> to a List<A> instead of IReadOnlyList<A> when adding an element will lead to an error anyway?

      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. Use List<> the proper way: List<A> listOfA = new List<A>()

  • Define a List of your interface

  • Assign instances of one derived class only - you didn't want to store other classes anyway, did you?

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

I personally like to create libs with extensions to the classes

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

You can also use the System.Runtime.CompilerServices.Unsafe NuGet package to create a reference to the same List:

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

Given the sample above, you can access the existing Hammer instances in the list using the tools variable. Adding Tool instances to the list throws an ArrayTypeMismatchException exception because tools references the same variable as hammers.

I've read this whole thread, and I just want to point out what seems like an inconsistency to me.

The compiler prevents you from doing the assignment with Lists:

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

But the compiler is perfectly fine with arrays:

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

The argument about whether the assignment is known to be safe falls apart here. The assignment I did with the array is not safe. To prove that, if I follow that up with this:

myAnimalsArray[1] = new Giraffe();

I get a runtime exception "ArrayTypeMismatchException". How does one explain this? If the compiler really wants to prevent me from doing something stupid, it should have prevented me from doing the array assignment.

Related