Converting derived type into base (GENERIC) type?

Hi,
I’m kinda new to the generic programming with C#, but I’m actually stuck on this strange behavior … any help?

Below is the generic class, which gets derived by the CommonAttMgr and SpecialAttMgr ONLY to pass the type.

public class GeneralAttMgr<t> : MonoBehaviour where t: IAttribute
{
    public List<t> AttributesList;

    public t GetAttributeByName(string name)
    {
        foreach (var att in AttributesList)
            if (att.AttName == name)
                return att;
        return default;
    }
}

public class CommonAttMgr : GeneralAttMgr<CommonAttr> { }

public class SpecialAttMgr : GeneralAttMgr<SpecialAttr> { }

And below are the classes that implement the interface IAttribute

public class GrandAttr : IAttribute
{
    public string AttName { get; set; }
    public Sprite AttSprite { get; set; }
}

public class CommonAttr : GrandAttr {
    public string CommonString { get; set; }
}

public class SpecialAttr : GrandAttr {
    public string SpecialString { get; set; }
}

But when the time comes to run the code, I have a base GameController which has the generic “GeneralAttMgr” instance, and would like to pick it as a component in the child classes with their derived types.

public class GameControl:MonoBehaviour
{
    protected GeneralAttMgr<GrandAttr> AttMgr;
}
public class SubGameControl : GameControl{
    void Start()
    {
        AttMgr = GetComponent<CommonAttMgr>()// // <<= Doesn't Work
    }
}
public class SubGameControl2 : GameControl{
    void Start()
    {
        AttMgr = GetComponent<SpecialAttMgr>() // <<= Doesn't Work
    }
}

I keep getting an error: Cannot implictly convert type CommonAttrMgr to GeneralAttrMgr …

Shouldn’t this just work? it’s very much like: Parent x = new Child()

I tried using a cast but it wasn’t helpful.

Please help

What you want is basically called covariance. You’re expecting that you should be able to assign a more derived generic type’s instance to that of a less derived generic. This is similar to how you’re able to assign an array of strings to a variable that’s typed as an array of objects. The code as posted doesn’t work, as you said, but something close can be approximated - there are at least two ways to approach this.

Note that it’s C# convention to capitalize the generic type parameter name.

First approach: if you’re only going to be reading from the list (and your writing logic is still based on specific instances, you can specify something like this.

IAttMgr<GrandAttr> mgr = new SpecialAttMgr();

public interface IAttMgr<out T> where T : IAttribute
{
    T GetAttributeByName(string name);
}

public class GeneralAttMgr<T> : MonoBehaviour, IAttMgr<T> where T : IAttribute
{
    public List<T> AttributesList;

    public T GetAttributeByName(string name)
    {
        foreach (var att in AttributesList)
            if (att.AttName == name)
                return att;
        return default;
    }
}

public class CommonAttMgr : GeneralAttMgr<CommonAttr>
{
}

public class SpecialAttMgr : GeneralAttMgr<SpecialAttr>
{
}

public interface IAttribute
{
    string AttName { get; set; }
}

public class GrandAttr : IAttribute
{
    public string AttName { get; set; }
    public Sprite AttSprite { get; set; }
}

public class CommonAttr : GrandAttr
{
    public string CommonString { get; set; }
}

public class SpecialAttr : GrandAttr
{
    public string SpecialString { get; set; }
}

In this example, I’m using an interface that specifically applies the out modifier to the type parameter, making it so that for a variable typed as IAttrMgr, any object of a type that implements IAttrMgr or IAttrMgr can be assigned to that variable.

The 2 main limitations with this is that you effectively lose write access when using a variable using the interface type, since the out modifier makes it so the type parameter’s type can only be used for method returns, though you can still do whatever you want with the instance inside the class itself or with a variable that’s at least as specific as GeneralAttMgr. The second limitation is that as I understand it, you also lose Unity’s semantics with null checking if the reference isn’t of a type derived from UnityEngine.Object, though you can still do something along the lines of this:

if (attMgr is UnityEngine.Object attMgrObj && attMgrObj != null)

Second approach: you can use a non-generic base class for the manager type.

GeneralAttMgr AttMgr = new CommonAttMgr();

public abstract class GeneralAttMgr : MonoBehaviour
{
    public abstract TValue GetAttributeByName<TValue>(string name) where TValue : IAttribute;
}

public class GeneralAttMgr<T> : GeneralAttMgr where T : IAttribute
{
    public List<T> AttributesList;
    public override TValue GetAttributeByName<TValue>(string name)
    {
        /*foreach (var att in AttributesList.OfType<TValue>())
            if (att.AttName == name)
                return att;*/
        foreach (var att in AttributesList)
            if (att is TValue att2 && att2.AttName == name)
                return att2;
        return default;
    }
}

public class CommonAttMgr : GeneralAttMgr<CommonAttr>
{
}
public class SpecialAttMgr : GeneralAttMgr<SpecialAttr>
{
}

public interface IAttribute
{
    string AttName{get;set;}
}

public class GrandAttr : IAttribute
{
    public string AttName { get; set; }
    public Sprite AttSprite { get; set; }
}
public class CommonAttr : GrandAttr
{
    public string CommonString { get; set; }
}
public class SpecialAttr : GrandAttr
{
    public string SpecialString { get; set; }
}

In this scenario, instead of using the same type parameter for the stored list and the return for GetAttributeByName, you simply make GetAttributeByName a typed method itself, and use the type testing operator to check each stored element in the list. If you had a GeneralAttrMgr variable that referenced a CommonAttMgr, you’d want to call GetAttributeByName to get any attributes of type GrandAttr (or derived) or GetAttributeByName to get any attributes of type CommonAttr (or derived). I don’t pretend to understand the entirety of what your code will be doing from just the code posted here, the choice would be up to you.

In general, I’d also recommend that you also implement the Try method pattern where your method returns a bool for success instead of returning default, especially for reference types. That would look like this:

public bool TryGetAttributeByName<TValue>(string name, out TValue value)

This simplifies cases where you need to do something with a valid instance of something and want to bail out to another code path when the thing you wanted doesn’t exist.

2 Likes

Double-casting with unboxing can also work in some situations, it bailed me more than once, especially in low-level value type conversions. However, yours has an interface assigned to component and I don’t claim this is even applicable, but

obj = (ConcreteType)(object)GetObject<DerivedType>();

I don’t recommend this path if a better design is available (it’s expensive, unless it’s something used only occasionally). Taking covariance into account by design as explained by dude_rtfm is a much better route.

There are also other techniques with covariance (or at least in straight C#) but I have to admit I need to research these things from scratch every single time, and while I like to write generic code, this has been my arch-nemesis, though I typically manage to solve it with only minor changes (and then the GC sweeps through my brain).

@Spy-Master Nice examples