Abstract method with strongly typed return type
Asked Answered
I

5

8

Consider the following classes :

public abstract class Animal
{
    public abstract Animal GiveBirth();
}

public class Monkey : Animal
{
    public override Animal GiveBirth()
    {
        return new Monkey();
    }
}

public class Snake : Animal
{
    public override Animal GiveBirth()
    {
        return new Snake();
    }
}

//That one doesnt makes sense.
public class WeirdHuman: Animal
{
    public override Animal GiveBirth()
    {
        return new Monkey();
    }
}

I'm searching a way to enforce the return types of the overrided GiveBirth method so that it always returns the actual class type, so that no WeirdHuman can give birth to a Monkey.

I feel like the answer is about generic types, but I can't see how I can do that.

Exemple of the expected result :

public abstract class Animal
{
    public abstract /*here a way to specify concrete type*/ GiveBirth();
}

public class Monkey : Animal
{
    public override Monkey GiveBirth() //Must returns an actual Monkey
    {
        return new Monkey();
    }
}

"Absolutely impossible" may be an answer, if clearly explained.

Interdict answered 23/4, 2013 at 14:34 Comment(0)
H
6

This is co-variant returns and is not supported by C#. I lament this daily. The best you can hope to do to get around it is to use a generic return type and specify a where condition on the generic type, but this can also cause you to run in to other issues down the road with matching generic parameter requirements.

public abstract class Animal<TBirthType> where TBirthType : Animal<TBirthType>
{
    public abstract TBirthType GiveBirth();
}

public class Monkey<TBirthType> : Animal<TBirthType> where TBirthType : Monkey<TBirthType>
{
    public override TBirthType GiveBirth()
    {
        return new Monkey<Monkey>();
    }
}

Alternately, if you don't need any further inheritance, you can close the generic.

public class Monkey : Animal<Monkey>
{
    public override Monkey GiveBirth()
    {
        return new Monkey();
    }
}

Note that covariance alone is still not enough to ensure that no misbehaving derived type can be formed, but it will allow for the type of the return to be specified as the type being used. There still wouldn't be a way to lock it down from the abstract class though. You could perhaps manage a runtime check via reflection from a method implemented at the base level that would check type at runtime, but this could also be very messy.

Hacksaw answered 23/4, 2013 at 14:37 Comment(7)
That's what I thought. I can't see any way to specify that the generic type must implement the actual class Type. Like where T : this or something...Interdict
What are the languages that support it?Interdict
Java supports it. Nothing .Net supports it as the CLR is the underlying problem. Hopefully a future version will add support, but as I understand it it would take a substantial change to make it work.Hacksaw
@Interdict - just updated my answer to include a version that closes the generic as well. This can be useful if you only need the one level of inheritance.Hacksaw
Return type co-variance isn't enough since there's no way of enforcing that the concrete type returned from GiveBirth matches the enclosing class type.Asterisk
@Asterisk - Ah, I missed that part of the question. I do believe that my answer is the closest that you can get in C#, though it means that it is something that I don't know if Java supports either.Hacksaw
Why do you use where TBirthType : Animal<TBirthType>? I don't understand why TBirthType is Animal<TBirthType>.You are using TBirthType as a restriction to TBirthType and also with the Aninal class. I don't understand it.Semitropical
B
3

You can do something like this, which forces the implementers of Animal<T> to implement an Animal<T> GiveBirth() method which returns the same type as the type parameter, which itself is constrained to be a kind of animal.

That's not quite what you want, but just so you can see:

public abstract class Animal<T> where T: Animal<T>
{
    public abstract Animal<T> GiveBirth();
}

public class Monkey: Animal<Monkey>
{
    public override Animal<Monkey> GiveBirth()
    {
        return new Monkey();
    }
}

public class Snake: Animal<Snake>
{
    public override Animal<Snake> GiveBirth()
    {
        return new Snake();
    }
}

public class WeirdHuman: Animal<WeirdHuman>
{
    public override Animal<WeirdHuman> GiveBirth()
    {
        return new Monkey(); // Won't compile of course.
    }
}

If you comment out the public override Animal<Monkey> GiveBirth() methods, you'll see that the compiler complains and says something like:

Error 1 'ConsoleApplication1.Monkey' does not implement inherited abstract member 'ConsoleApplication1.Animal.GiveBirth()'

Unfortunately, you must declare the classes using the SomeKindOfAnimal: Animal<SomeKindOfAnimal> syntax, but maybe this will work for you.

(Also see this thread.)

Alas, this doesn't quite work because it allows you to do this:

public class Monkey: Animal<WeirdHuman>
{
    public override Animal<WeirdHuman> GiveBirth()
    {
        return new WeirdHuman();
    }
}

In other words, it constrains the type parameter to be a kind of animal, and it also constrains the return type of GiveBirth() to be the same as the type parameter; but that's all it does. In some cases this is enough, but probably not for your purposes.

Still, perhaps this approach is worth knowing about.

Bedraggled answered 23/4, 2013 at 14:46 Comment(4)
And then what happens when someone makes a Monkey : Animal<WeirdHuman> and makes really messed up babies?Rhoads
@Rhoads :) Well, the GiveBirth() method is still constrained to only return a Monkey, so that's fulfilling the requirement for "a way to enforce the return types of the overrided GiveBirth method so that it always returns the actual class type" (from the OP). You're allowed to say that a Monkey is a WeirdHuman if you want!Bedraggled
But it's not constrained to return the type of the class implementing the interface, just any type that the class implementing the interface chooses.Rhoads
@Servy: Yes true; the type parameter is constrained to be some kind of animal, and the GiveBirth() return type is constrained to be the same as the type parameter, and that is all.Bedraggled
A
3

As far as I know, there is no clean way to support this purely in a single class hierarchy. Using recurring generic type parameters e.g.

public class Animal<T> where T : Animal<T> { }

may be acceptable if you control the entire hierarchy, and can therefore rule out classes like

public class WierdHuman<Monkey> { }

What you really want is something like Haskell's typeclasses, where you can abstract over the concrete type of the class itself. The closest you can get in C# is to define a surrogate object which implements the required functionality, and then pass that around wherever you require it.

In your case, this means creating an interface for giving birth, and implementing it for each concrete animal type.

Your methods which require this functionality then need an extra parameter for the 'typeclass instance'. These methods can restrict the generic animal type to be the same:

public interface ISpawn<T> where T : Animal
{
    public T GiveBirth();
}

public void Populate<T>(T parent, ISpawn<T> spawn) where T : Animal
{
}
Asterisk answered 23/4, 2013 at 15:4 Comment(0)
F
2

As of .NET 5 / c# 9 Covariant returns are allowed and so this is supported. If I paste this code into dotnetfiddle with .NET 5 it works (Notice the return types of GiveBirth on the various subtypes):

using System;

public class Program
{
    public static void Main()
    {
        Console.WriteLine(new Snake().GiveBirth());
    }
}

public abstract class Animal
{
    public abstract Animal GiveBirth();
}

public class Monkey : Animal
{
    public override Monkey GiveBirth()
    {
        return new Monkey();
    }
}

public class Snake : Animal
{
    public override Snake GiveBirth()
    {
        return new Snake();
    }
}

//That one doesnt makes sense.
public class WeirdHuman: Animal
{
    public override Monkey GiveBirth()
    {
        return new Monkey();
    }
}

If I try with .NET Core 3.1 it does not compile and gives the following error message

Compilation error (line 26, col 27): 'Snake.GiveBirth()': Target runtime doesn't support covariant return types in overrides. Return type must be 'Animal' to match overridden member 'Animal.GiveBirth()'

Compilation error (line 35, col 28): 'WeirdHuman.GiveBirth()': Target runtime doesn't support covariant return types in overrides. Return type must be 'Animal' to match overridden member 'Animal.GiveBirth()'

Compilation error (line 18, col 28): 'Monkey.GiveBirth()': Target runtime doesn't support covariant return types in overrides. Return type must be 'Animal' to match overridden member 'Animal.GiveBirth()'

Fourpenny answered 29/9, 2023 at 6:34 Comment(0)
G
0

If you have an situation where your base class cannot be generic for various reasons, this method might be useful:

abstract class Animal {
}
interface ICanGiveBirth<T> {
   T GiveBirth();
}
static class CanGiveBirthHelper {
   public static T GiveBirth<T>(this T v) where T: ICanGiveBirth<T> => v.GiveBirth();
}
class Monkey : Animal, ICanGiveBirth<Monkey> {
   public Monkey GiveBirth() {
      throw new NotImplementedException();
   }
}
class Snake : Animal, ICanGiveBirth<Snake> {
   public Snake GiveBirth() {
      throw new NotImplementedException();
   }
}

If you are unable to add interface to your sub classes, and still unable to add generics to the Base type this method might be useful: (Unfortunately you cannot make the GiveBirthImpl protected, since the helper class is not allowed to be inside the base class)

abstract class Animal {
   public abstract T GiveBirthImpl<T>() where T:Animal;
}
static class CanGiveBirthHelper {
   public static T GiveBirth<T>(this T v) where T: Animal => v.GiveBirthImpl<T>();
}
class Monkey : Animal {
   public override T GiveBirthImpl<T>() {
      throw new NotImplementedException();
   }
}
class Snake : Animal {
   public override T GiveBirthImpl<T>() {
      throw new NotImplementedException();
   }
}

In both cases, this will work as expected:

class Tester
{
   Monkey TestIt() => new Monkey().GiveBirth();
}
Gynecology answered 5/8, 2021 at 8:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.