Search code examples
c#oopunity-game-enginegenericsabstraction

A little bit confusion with C# Generics, T, Abstraction ?


I am a C# game maker with Unity. I have collectable management system.

CollectableManager

public List<CollectableParent<Collectable>> collactableParentsList;

CollectableParent

public class CollectableParent<T> : CollectableRelatedMonoBehaviour where T : Collectable

SpawnPointDefinedCollectableParent

public class SpawnPointParentDefinedCollectableParent<T> : CollectableParent<T> where T : Collectable

Collectable

public abstract class Collectable : CollectableRelatedMonoBehaviour, IHasPlayableSound

Collectable_Money

public class Collectable_Money : Collectable

CollectableParent_Money

public class CollectableParent_Money : SpawnPointParentDefinedCollectableParent<Collectable_Money>

PROBLEM

"collactableParentsList" in CollectableManager does not accept SpawnPointParentDefinedCollectableParent<Collectable_Money> as item when T is defined as "Collectable_Money", it is derived from "Collectable". If I do this SpawnPointParentDefinedCollectableParent<Collectable>, it is accepted as item into list.


Solution

  • The problem here is that even though Collectable_Money is a sub-class of Collectable, that does not make SpawnPointParentDefinedCollectableParent<Collectable_Money> a sub-class of CollectableParent<Collectable>.

    It may be possible to fix this by using generic interfaces. If we change CollectableParent<T> to be an interface, and define it to be covariant in T via the out modifier. (You haven't provided the definition for CollectableRelatedMonoBehaviour, but it will also need to be converted to an interface):

    public interface ICollectableParent<out T> : ICollectableRelatedMonoBehaviour where T : Collectable
    

    If you then define the list as:

    public static List<ICollectableParent<Collectable>> collectableParentsList;
    

    Then you can successfully add items of type SpawnPointParentDefinedCollectableParent<Collectable_Money> to it.

    The critical part here is that the ICollectableParent<T> interface is covariant in T. This makes it possible to pass an instance that implements ICollectableParent<Collectable_Money> where an ICollectableParent<Collectable> is expected.

    Defining the interface as covariant in T does introduce some restrictions, specifically (and as implied by the out modifier used to indicate covariance), the type T must only be used as a return value on the interface's methods. For example:

    public interface ICollectableParent<out T> : ICollectableRelatedMonoBehaviour where T : Collectable
    {
        // This is allowed - T is used as a return type
        T GetChild();
        // This is *not* allowed - T is used as a parameter
        void SetChild(T child);
    }
    

    You can read more about covariance, and its inverse (contravariance) here: Difference between Covariance & Contra-variance