0

C# supports return type covariance, but why does it not support parameter type contravariance.

Imagine this example:

abstract class Animal
{
    public abstract void PlayWith(Toy toy);
}

class PlayfulMonkey : Animal
{
    public override void PlayWith(Object toy) // PlayfulMonkey can play with anything
    {
        // Monkey playing with its object...
    }
}

C# complier throws an error at you when trying this. But I feel like it would make so much sense to allow this.

Anitoni
  • 37
  • 4
  • Why? How would that work if you passed in a string as the argument? `mokey.PlayWith("abc");` – gunr2171 Aug 21 '23 at 18:39
  • 2
    Why? Because the designers decided not to include that feature. – Heretic Monkey Aug 21 '23 at 18:39
  • 1
    How would you use that if your playful monkey was stored in a variable of type `Animal`? You would need to `if(animal is PlayfulMonkey playful_monkey) playful_monkey.PlayWith(not_a_toy)`, wouldn't you? At which point the entire inheritance/overriding thing goes out of the window? – GSerg Aug 21 '23 at 18:41
  • @GSerg Well if it is stored in a `Animal` type variable then you can pass in only a `Toy`. But that is ok, a toy is still an object. – Anitoni Aug 21 '23 at 18:44
  • 1
    It sounds like you're looking for components (ECS for example), not inheritance. What you wrote there makes no sense in traditional polymorphism. – Blindy Aug 21 '23 at 18:45
  • @Blindy: No Sense, Really? It nicely follows the _Open-Closed Principle_ (the **O** in Solid) : _"software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification"_ – Flydog57 Aug 21 '23 at 19:05
  • 1
    No it doesn't, what are you talking about? He's literally talking about *modifying* the parent's contract -- look at the answer below that achieves what he wants by adding a second method (not part of the polymorphic hierarchy, and thus unavailable through a handle to the parent) and ignoring the actual polymorphic method. – Blindy Aug 21 '23 at 19:10
  • 1
    This kind of "yeah, my cat is a dog except for ...." inheritance really shows both that inheritance probably shouldn't have been used in the first place (again, use components), and that the person in question simply doesn't understand inheritance. – Blindy Aug 21 '23 at 19:11
  • 1
    related: [Contravariant method argument type](https://stackoverflow.com/q/22586976/1371329) and also: [Why is there no parameter contra-variance for overriding?](https://stackoverflow.com/q/2995926/1371329) – jaco0646 Aug 21 '23 at 21:22

2 Answers2

14

A "why not" question presupposes that the feature is so obviously good that the designers should provide you a reason to not spend their time, money and effort to implement the feature. But that's not how language design works. The number of possible features is huge, and the onus is on the person requesting the feature to explain not just why it's a good feature, but why it is BETTER than every other feature the design team could be spending time on.

People asked for return type covariance for literally two decades and it took that long to get to the top of the priority list. Why did it take that long to get return type covariance? Because (1) the design team considered it low priority, (2) the implementation is not trivial because the underlying type system in the CLR does not support virtual variance, and (3) there were always higher priority work items that we believed were more bang for buck.

Would parameter type contravariance make sense? Sure. Is anyone other than you asking for it? Not to my knowledge. "It makes sense" is nowhere near a good enough reason; millions of features "make sense" but only a handful get implemented in any version.

It's not the design team's job to explain to you why your pet feature isn't on top of their priority list. If you want this feature, the onus is on you to go to the roslyn github, find the existing design request if there is one, open a new one if there is not, and make a compelling argument that this is the most important thing that the design team should be spending time on. In your argument I recommend that you focus heavily on all the real-world problems that would be solved for professional line-of-business developers by adding this feature. I'm not aware of any, but maybe you are.

ADDENDUM: I forgot to mention, if you do make an argument for this feature you should anticipate the following pushback. Suppose team Alpha writes this:

class Alpha
{
  public virtual void M(Giraffe g) { alpha implementation }
}

And then team Bravo says let's extend Alpha:

class Bravo : Alpha
{
  public override void M(Giraffe g) { bravo implementation }
}

and then team Charlie says let's extend Bravo:

class Charlie : Bravo
{
  public override void M(Giraffe g) { charlie implementation }
}

And then team Bravo says oh, wait, we can do better, our method actually accepts any mammal, let's use the new parameter type contravariance feature, and they change to:

class Bravo : Alpha
{
  public override void M(Mammal g) { bravo implementation }
}

Does team Charlie get a build break when they pick up the latest version of Bravo? Why or why not? If the Charlie implementation only handles giraffes, are they required to now handle any mammal just because Bravo can?

C# designers try to avoid adding new versions of the "brittle base class" failure mode; they will if there is a compelling benefit, so again, identify that compelling benefit in your argument for why taking on a new form of versioning failure is acceptable.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
1

You can't change the method signature in an override but you can overload the method:

class PlayfulMonkey : Animal
{
    public override void PlayWith(Toy toy) 
    {
        // Monkey playing with its toy...
    }
    
    public void PlayWith(Object obj) // PlayfulMonkey can play with anything
    {
        // Monkey playing with its object...
    }
}

Note that the overload is a completely different method that only applies to PlayfulMonkey, and you can decide how to connect the two methods if you want (e.g. reroute to the Toy version if the object is a Toy, or pass the toy instance from the Toy method to the Object method).

D Stanley
  • 149,601
  • 11
  • 178
  • 240
  • Is there a specific reason that it is not possible to "upcast" the parameter? Would it mess up my code? – Anitoni Aug 21 '23 at 18:56
  • 1
    It's worth noting that this answer achieves what you are looking for. Under your design, you could only access the `PlayfulMonkey.PlayWith(object toy)` directly through a `PlayfulMonkey` reference, not an `Animal` reference (since the `Animal` method can only take a `Toy` as a parameter). This is likely why the _"designers decided not to include that feature"_ (to quote the well-named @heriticmonkey) – Flydog57 Aug 21 '23 at 19:01
  • @Anitoni Upcasting is not safe in general (you can't assume that an `Object` is a `Toy`) so you have not fulfilled the `Animal` signature that _requires_ a `Toy`. – D Stanley Aug 21 '23 at 19:08