Thursday, June 7, 2018

Covariance and Contravariance in C#

Covariance and Contravariance are two crazy terms that I've only ever really seen come up with Resharper tells me I'm doing something questionable. But then I was on an interview and somebody assumed - based on my other answers - that I knew what they were, but I didn't. So I figured I should probably learn.

Let me start off by saying that I've found this fantastic answer on Stack Overflow (I seriously probably spend too much time there). StuartLC goes into the right amount of detail on a really good explanation of what Covariance and Contravariance are and how and when to use them. I'm going to copy parts of his answer here just in case something ever happens to that post on SO and I can't go back and reference it.

Microsoft actually has an answer that's pretty good, if you can get past the big words and whatnot: "In C#, covariance and contravariance enable implicit reference conversion for array types, delegate types, and generic type arguments. Covariance preserves assignment compatibility and contravariance reverses it." Well, it's a good answer until that last part (at least for me) when it mentions that Covariance preserves assignment compatibility and Contravariance reverses it. I read that sentence a hundred times and still had no idea what it meant.

So let me break it down as simply as I can. Covariance enables collections (such as arrays), delegate types, and generic type arguments of a more-derived type to a collection, delegate type, and generic type argument of a less-derived type. Let's say we have the following classes:
   1:  public class LifeForm { }
   2:  public class Animal : LifeForm { }
   3:  public class Giraffe : Animal { }
   4:  public class Zebra : Animal { }

Animal is a more-derived type than LifeForm (because Animal inherits LifeForm) while Zebra and Giraffe are more-derived types than Animal (becuase they inherit Animal).

If we then have the following interface with a generic type argument:
public interface IDoStuff<T> { }
and the following class that implements that interface:
public class StuffDoer<T> : IDoStuff<T> { }
we can't do this:
   1:  static void Main(string[] args)
   2:  {
   3:      IDoStuff<LifeForm> animal = new StuffDoer<Animal>();
   4:  }

That doesn't work because Animal is a more derived type than LifeForm and right now IDoStuff is Invariant and not Covariant. By making a small change our code will work. In our IDoStuff interface we just have to enable Covariance by adding the out keyword, like this:
public interface IDoStuff<out T> { }

Now that we've established what Covariance is and means (enabling implicit conversion from a more-derived type to a less-derived type) what is it good for? That brings me back to the SO answer I linked at the beginning of this post. StuartLC writes "Covariance is widely used with immutable collections (i.e. where new elements cannot be added or removed from a collection)". It's simple, but it's worth diving more into.

Consider the use of IList, which is Invariant. We have a method that accepts an IList of type LifeForm:
   1:  public void PrintLifeForms(IList<LifeForm> lifeForms)
   2:  {
   3:      foreach (var lifeForm in lifeForms)
   4:      {
   5:          Console.WriteLine(lifeForm.GetType().ToString());
   6:      }
   7:  }

We should be (and are) able to pass in a "heterogeneous collection" (i.e. a collection of objects that are derived from LifeForm, but aren't necessarily LifeForm itself), so this would work:
   1:  IList<LifeForm> animals = new List<LifeForm>
   2:  {
   3:      new Giraffe();
   4:      new Zebra();
   5:      new Animal();
   6:  };
   7:  
   8:  PrintLifeForms(animals);
but this would fail:
   1:  IList<Giraffe> giraffes = new List<Giraffe>
   2:  {
   3:      new Giraffe();
   4:      new Giraffe();
   5:      new Giraffe();
   6:  };
   7:  
   8:  PrintLifeForms(giraffes);

I know for me at least, this doesn't seem right. It seems like as a collection of a derived type I should be able to pass it to a parameter accepting a collection of a less-derived type. But I can't. Not when I'm using an IList anyway, because IList is invariant. Now, my ah-ha moment was in the next part of the SO answer when he wrote "If I maliciously change the method implementation of PrintLifeForms (but leave the same method signature), the reason why the compiler prevents passing List becomes obvious:"

   1:  public void PrintLifeForms(IList<LifeForm> lifeForms)
   2:  {
   3:      lifeForms.Add(new Zebra());
   4:  }

As soon as I saw that code sample, it clicked. As long as my IList is LifeForm objects I can add a zebra no problem, but if I pass in an IList of giraffes then I can't add a zebra to my list because a zebra is not a giraffe.

In this particular case, implementations of IList are allowed to have items added to them so we want IList to be invariant. In other words we don't want to accidentally expect implicit conversion from a list of Giraffes to a list of LifeForms because then we'd break if we added a zebra. If we want to be able to pass in a List of Giraffe objects then we should modify our parameter to accept an IEnumerable of LifeForm objects because IEnumerable uses a covariant generic type:
   1:  public void PrintLifeForms(IEnumerable<LifeForm> lifeForms)
   2:  {
   3:      foreach (var lifeForm in lifeForms)
   4:      {
   5:          Console.WriteLine(lifeForm.GetType().ToString());
   6:      }
   7:  }

And just like that we can now pass in our List of Giraffe objects without a problem.

That was all to explain Covariance, but the other half of this post is about Contravariance so I guess we should get to that. Contravariance is the inverse of Covariance. That is, while Covariance enables a more-derived type to be provided when a less-derived type is expected, Contravariance enables a less-derived type to be provided when a more-derived type is expected. But, what does that mean?

Let's say we have this method:
   1:  public void DoSomething(IDoStuff<Zebra> doer)
   2:  {
   3:      doer.WriteMessage($"{doer.GetType()} was passed");
   4:  }

We can pass an instance of IDoStuff of type Zebra to the method... I guess that's obvious. But we can't pass an instance of IDoStuff of type Animal or Giraffe or LifeForm. That's because right now IDoStuff is Invariant (I reverted it from earlier) and it looks like this:
   1:  public interface IDoStuff<T>
   2:  {
   3:      void WriteMessage(string message);
   4:  }
and it's implemented in StuffDoer like this:
   1:  public class StuffDoer<T> : IDoStuff<T>
   2:  {
   3:      void WriteMessage(string message)
   4:      {
   5:          Console.WriteLine(message);
   6:      }
   7:  }

Right now, this would work:
DoSomething(new StuffDoer<Zebra>());
but this wouldn't:
DoSomething(new StuffDoer<Animal>());

If we want the second call to work we need to enable Contravariance on IDoStuff. We can do that by using the in keyword, like this:
   1:  public interface IDoStuff<in T>
   2:  {
   3:      void WriteMessage(string message);
   4:  }

Now we can pass an instance of DoerStuff to PerformZebraAction. Why would we do this? What would be the purpose of enabling Contravariance? Going back to StuartLC's answer we see "Contravariance is frequently used when functions are passed as parameters." He uses Action to explain the point. Using our example with giraffes and zebras (which is actually his example) it makes a little bit more sense. Consider this method:

   1:  public void PerformZebraAction(Action<Zebra> zebraAction)
   2:  {
   3:      var zebra = new Zebra();
   4:      zebraAction(zebra);
   5:  }

We can see that passing in an instance of Action would be perfectly acceptable, but we can also pass in an instance of Action. Both of these work:
   1:  var zebraAction = new Action<Zebra> (z => Console.WriteLine("I'm a zebra!"));
   2:  PerformZebraAction(zebraAction);
   3:  
   4:  var animalAction = new Action<Animal> (z => Console.WriteLine("I'm an animal!"));
   5:  PerformZebraAction(animalAction);

but this won't work:
   1:  var giraffeAction = new Action<Giraffe> (z => Console.WriteLine("I'm a giraffe!"));
   2:  PerformZebraAction(giraffeAction);

If we just think about what we're asking for here, we're saying we want the giraffe to do zebra stuff. That doesn't make sense. But we could ask a generic animal to do zebra stuff.


At the end of the day, in 12+ years of writing C# I've never really had to worry about Covariance or Contravariance. But now at least I understand what they mean and do so I'm a bit better informed. Hopefully I remember I wrote this post so I can reference it if I ever get confused again.

No comments:

Post a Comment