Home > front end >  Cannot implicitly convert child class to parent class in type parametrized method
Cannot implicitly convert child class to parent class in type parametrized method

Time:09-17

I recently had to help troubleshoot an issue someone had with returning from a generic method, and while there were multiple problems to be solved, I understood and could explain all of them - except for the last hurdle of getting the compiler to accept the return type. Though I eventually succeeded in getting the program to compile and run correctly, I still can't completely follow the logic of why it had to be done this way.

I've reproduced the issue with a minimal example below. Given a very simple parent-child class structure with only a single generic method:

abstract class AbstractParentClass
{
    public abstract T DoThing<T>() where T : AbstractParentClass;
}

class ConcreteChildClass : AbstractParentClass
{
    public override T DoThing<T>()
    {
        return this;
    }
}

This will cause an error on the return line of Cannot implicitly convert type 'ConcreteChildClass' to 'T'. Slightly strange given that T is constrained to be an instance of AbstractParentClass and this trivially is one of those but okay, sure, so we'll do it explicitly:

public override T DoThing<T>()
{
    return (T) this;
}

Now the error message reads Cannot convert type 'ConcreteChildClass' to 'T'. What? If T were unconstrained then sure, I get that we couldn't guarantee it, but with an inheritance like we do, surely it should be a straightforward cast?

We can get close by explicitly casting to the parent class:

public override T DoThing<T>()
{
    return (AbstractParentClass) this;
}

Now the error reads Cannot implicitly convert type 'AbstractParentClass' to 'T'. An explicit conversion exists (are you missing a cast?). Why can we not implicitly convert to the exact type of the constraint - what possible situation could lead to a class of the constraint type to not be convertable to... itself? But at least this is solvable now, the error message even tells us directly how to do it:

public override T DoThing<T>()
{
    return (T)(AbstractParentClass) this;
}

And now everything runs just fine. We can even make it look slightly prettier if we want:

public override T DoThing<T>()
{
    return this as T;
}

Throughout all of this, if T were unconstrained, I of course understand why these conversions wouldn't be possible as written. But it is, and in a way that the compiler should not have any issue following. Does the compiler, for some reason, just not take type constraints into account for allowable implicit conversions? In my experience all situations like this in C# have a very good reason behind them, I just for the life of me can't figure it out for this one.

If anybody has any insight into why the compiler has trouble with this (seemingly!) very simple bit of code, I'd be thankful for an explanation of the problems with allowing these conversions.

CodePudding user response:

Becasue technically you could do:

class ConcreteChildClass2 : AbstractParentClass
{
    public override T DoThing<T>()
    {
        return null;
    }
}

var ccc = new ConcreteChildClass();
var ccc2 = ccc.DoThing<ConcreteChildClass2>();

Even with the casts you do, this will blow up at runtime:

System.InvalidCastException: Unable to cast object of type 'ConcreteChildClass' to type 'ConcreteChildClass2'.

If instead you do return this as T, you'll get null as a result instead of a casting exception.

In other words, the constraint does not guarantee that DoThing returns the same type as the defining class, it only guarantees that it returns some type that inherits AbstractParentClass.

CodePudding user response:

The "why" is that even though ConcreteChildClass is an AbstractParentClass and T is an AbstractParentClass, that does not necessarily guarantee that ConcreteChildClass is a T.

For a real-world analogy, Cat is a Pet and Dog is a Pet, but Cat is not a Dog.

D Stanley's answer shows how you could invoke DoThing() with a value for T that wouldn't be correct for the type of object you're dealing with.

Depending on your use case, there are some other patterns you could try using to solve this problem.

For example, you could use a recursive enum declaration type on your parent class, similar to what Java does for its Enums:

abstract class AbstractParentClass<T> where T : AbstractParentClass<T>
{
    public abstract T DoThing();
}

class ConcreteChildClass : AbstractParentClass<ConcreteChildClass>
{
    public override ConcreteChildClass DoThing()
    {
        return this;
    }
}

This makes it so you can't tell a ConcreteChildClass to use a different class as its generic type when you invoke DoThing(). You enforce that any child class that follows the pattern correctly must return an instance of the right type. However, it forces you to propagate the generic type everywhere: you can't just refer to an AbstractParentClass without including some generic parameter. You can get around that with a non-generic interface for code to use it doesn't care which implementation type it's given. But it's still a little clunky because (unlike Java Enums) you can't really enforce that every implementation extends the parent class and uses itself as the generic type.

  • Related