Tuesday, 5 June 2012

C# vs Java: Return Type Covariance

Covariance and Contravariance is a complicated topic that occasionally comes up again while coding something and as a consequence has me searching back into my memories to retrieve lessons learnt about it in the past. In fact, a long while ago I started to write a long post/self help document, but never managed to finish it (so far)

The topic showed up again last week, while dealing with method hiding. I came across several comments in StackOverflow stating that the main use of method hiding in C# is to simulate return type covariance. Return type covariance allows us to "narrow" the return type of an overriding method in the derived class, that is:

public class Foo {
}
 
public class SubFoo extends Foo {
}
 
public class Bar {
    final Foo foo = new Foo();
    public Foo getFoo() {
        return foo;
    }
}
 
public class SubBar extends Bar {
    final SubFoo subFoo = new SubFoo();
 
    @Override
    public SubFoo getFoo() {
        return subFoo;
    }
}

I've taken the above correct Java code (Java supports return type covariance since version 5) from here. Unfortunately, the equivalent C# version won't compile

So well, what about the idea of working around this limitation with Method Hiding?
Well, in principle that seems OK when the method being hidden is not virtual. Let's say we have an instance of the Child class, being pointed by a parent class variable. The call will end up being dispatched to the method in the Parent class, with in this case comes as not surprise, as we know static dispatch is being applied. Nonetheless, if we used method hiding for a virtual method in the Parent class (that's the case 2 in my previous article) we again will end up calling to the Parent method (cause the slot in the Child vTable is pointing there), which many people could consider as an unwanted outcome. What is for sure is that it's confusing.

Anyway, after some fast thinking on this I came up with this naive way to circumvent this limitation

public class Location
{
  public string Name{get;set;}
}

public class City: Location
{
  public int Population{get;set;}
}

public class Animal
{
  public virtual Location GetFavoritePlace()
  {
   return new Location(){Name = "forest"};
  }
}

public class Person: Animal
{
  public override Location GetFavoritePlace()
  {
   return this.GetFavoriteCity();
  }
 
  public City GetFavoriteCity()
  {
   return new City(){Name = "Berlin", Population = 4000000};
  }
}

Well, in fact it does not look that bad. We're keeping the polymorphism. Someone calling into GetFavoritePlace with an Animal variable pointing to a Person instance will still get a City object as return, which is something I consider fundamental to regard this as a valid option. On the other side, if someone is really expecting a City it's because he knows that the object on which he is doing the invocation is a Person, so he can do a downcast and call to GetFavoriteCity

After writing that and feeling partially satisfied, I decided to search a bit what others had to say on this matter, and I found this brilliant solution, that I've used below to reimplement the previous sample

public class Animal
{
  public Location GetFavoritePlace()
  {
   return this.GetFavoritePlaceImplementation();
  }
  
  protected virtual Location GetFavoritePlaceImplementation()
  {
   return new Location(){Name = "forest"};
  }
}

public class Person: Animal
{
  public new City GetFavoritePlace()
  {
   return (City)this.GetFavoritePlace();
  }
 
  protected override Location GetFavoritePlace()
  {
   return new City(){Name = "Berlin", Population = 4000000};
  }
}

An interesting question stems up from all this, why doesn't C# support return type covariance for normal methods (it does support it for Delegates since C# 2.0 and for Generic Interfaces since C# 4.0). I can't think of any real reason for this (remember that Java supports it), and after much googling all I've found is that it's not supported at the CLR level, and some comments by Eric Lippert that confirm the problem but don't really explain the reason (also Jon Skeet does some mention to methods returning void, but I don't fully grasp the implications)

No comments:

Post a Comment