Search code examples
c#genericsinheritanceinvariance

C# generic inheritance: invariance getting in the way of calling derived class


What I want to do:

abstract class TileBase
{
    protected TileGroup<TileBase> tileGroup;
}
class Tile : TileBase
{
    public Tile(Province Province)
    {
        tileGroup = Province;
    }
}
abstract class TileGroup<T>
{
    protected T[] tiles;
    protected TileGroup<TileGroup<T>> tileGroup;
}
class Province : TileGroup<TileBase>
{

    public Province(Tile tile, Nation nation)
    {
        tiles = new[] { tile };
        tileGroup = nation;
    }
}
class Nation : TileGroup<Province>
{

    public Nation(Province province)
    {
        tiles = new[] { province };
        tileGroup = null;
    }
}

This will not work because of invariance (if I understand invariance correctly): cannot convert Nation to TileGroup<TileGroup<TileBase>>

So I'll need to write it like this:

class Nation : TileGroup<TileGroup<TileBase>>
{

    public Nation(Province province)
    {
        tiles = new[] { province };
        tileGroup = null;
    }
}

But when layers get stacked; this gets ugly fast:

Map : TileGroup<TileGroup<TileGroup<TileBase>>> 

This also makes adding layers between two existing layers difficult because one change in a low layer means changing all the higher layers.

So how exactly should I be doing this?


Sorry for the formulation, I know what I want, but not how I should explain it clearer than in this way.


Solution

  • In order to understand what is going on and why this isn’t working, you need to understand what generic types actually are. In a way, they are a kind of template that provide exact types for every valid T as they are requested by the language.

    When you write TileGroup<TileBase> what actually happens is that a new type, let’s call it TileGroup_TileBase is defined that looks like this:

    class TileGroup_TileBase
    {
        protected TileBase[] tiles;
        protected TileGroup<TileGroup<TileBase>> tileGroup;
    }
    

    Now, let’s continue this expansion of generic types for a bit. We already know the TileGroup<TileBase> type in there, but there’s also a TileGroup<TileGroup_TileBase>, so let’s replace that:

    class TileGroup_TileBase
    {
        protected TileBase[] tiles;
        protected TileGroup_TileGroup_TileBase tileGroup;
    }
    class TileGroup_TileGroup_TileBase
    {
        protected TileGroup_TileBase[] tiles;
        protected TileGroup<TileGroup<TileGroup_TileBase>> tileGroup;
    }
    

    We could continue here, but this is actually enough to explain the problem. Let’s take a look at Nation instead which is what you are trying to assign in the Province constructor. Nation is a TileGroup<Province>, so let’s expand that:

    class TileGroup_Province
    {
        protected Province[] tiles;
        protected TileGroup<TileGroup<Province>> tileGroup;
    }
    

    Okay, so we have the types expanded enough. Let’s take a look at the assignment you are trying to do. In Province, the tileGroup property is of type TileGroup<TileGroup<TileBase>>, so essentially this is what you are trying to do:

    TileGroup<Province> nation = null;
    TileGroup<TileGroup<TileBase>> province_tileGroup = nation;
    

    Can you see why this fails now? If not, let’s use our expanded generic types here instead:

    TileGroup_Province nation = null;
    TileGroup_TileGroup_TileBase province_tileGroup = nation;
    

    Okay, that are the actual types that are used (remember that we are not doing this just to understand this, but that those generic types get actually materialized for each T for real!). But if we look at the definitions above, TileGroup_Province and TileGroup_TileGroup_TileBase are not actually related. Sure, they look similar, but there is no type relationship that would allow this kind of assignment!

    That’s actually why when we deal with generic types in the BCL, we so often have interfaces. Because interfaces allow us to have a type relationship between those generic type materializations which we can then use to assign one to another. To be honest though, your abstract classes with those protected fields make it a bit difficult to clean this up with interfaces, so you should probably think about whether you actually need this kind of type relationships and whether having an abstract base type for those fields is actually necessary.