Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Understanding Covariant and Contravariant interfaces in C#

I've come across these in a textbook I am reading on C#, but I am having difficulty understanding them, probably due to lack of context.

Is there a good concise explanation of what they are and what they are useful for out there?

Edit for clarification:

Covariant interface:

interface IBibble<out T> . . 

Contravariant interface:

interface IBibble<in T> . . 
like image 445
NibblyPig Avatar asked Apr 27 '10 09:04

NibblyPig


People also ask

What is difference between covariance and contravariance?

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.

How do you declare a generic interface as contravariant?

You can declare a generic type parameter contravariant by using the in keyword. The contravariant type can be used only as a type of method arguments and not as a return type of interface methods. The contravariant type can also be used for generic constraints.

Can you be declared as contravariant?

A type can be declared contravariant in a generic interface or delegate only if it defines the type of a method's parameters and not of a method's return type. In , ref , and out parameters must be invariant, meaning they are neither covariant nor contravariant.

Why is Ienumerable covariant?

No, because covariance is only allowed on interfaces that return values of that type, but don't accept them.


1 Answers

With <out T>, you can treat the interface reference as one upwards in the hierarchy.

With <in T>, you can treat the interface reference as one downwards in the hiearchy.

Let me try to explain it in more english terms.

Let's say you are retrieving a list of animals from your zoo, and you intend to process them. All animals (in your zoo) have a name, and a unique ID. Some animals are mammals, some are reptiles, some are amphibians, some are fish, etc. but they're all animals.

So, with your list of animals (which contains animals of different types), you can say that all the animals have a name, so obviously it would be safe to get the name of all the animals.

However, what if you have a list of fishes only, but need to treat them like animals, does that work? Intuitively, it should work, but in C# 3.0 and before, this piece of code will not compile:

IEnumerable<Animal> animals = GetFishes(); // returns IEnumerable<Fish> 

The reason for this is that the compiler doesn't "know" what you intend, or can, do with the animals collection after you've retrieved it. For all it knows, there could be a way through IEnumerable<T> to put an object back into the list, and that would potentially allow you to put an animal that isn't a fish, into a collection that is supposed to contain only fish.

In other words, the compiler cannot guarantee that this is not allowed:

animals.Add(new Mammal("Zebra")); 

So the compiler just outright refuses to compile your code. This is covariance.

Let's look at contravariance.

Since our zoo can handle all animals, it can certainly handle fish, so let's try to add some fish to our zoo.

In C# 3.0 and before, this does not compile:

List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal> fishes.Add(new Fish("Guppy")); 

Here, the compiler could allow this piece of code, even though the method returns List<Animal> simply because all fishes are animals, so if we just changed the types to this:

List<Animal> fishes = GetAccessToFishes(); fishes.Add(new Fish("Guppy")); 

Then it would work, but the compiler cannot determine that you're not trying to do this:

List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal> Fish firstFist = fishes[0]; 

Since the list is actually a list of animals, this is not allowed.

So contra- and co-variance is how you treat object references and what you're allowed to do with them.

The in and out keywords in C# 4.0 specifically marks the interface as one or the other. With in, you're allowed to place the generic type (usually T) in input-positions, which means method arguments, and write-only properties.

With out, you're allowed to place the generic type in output-positions, which is method return values, read-only properties, and out method parameters.

This will allow you to do what intended to do with the code:

IEnumerable<Animal> animals = GetFishes(); // returns IEnumerable<Fish> // since we can only get animals *out* of the collection, every fish is an animal // so this is safe 

List<T> has both in- and out-directions on T, so it is neither co-variant nor contra-variant, but an interface that allowed you to add objects, like this:

interface IWriteOnlyList<in T> {     void Add(T value); } 

would allow you to do this:

IWriteOnlyList<Fish> fishes = GetWriteAccessToAnimals(); // still returns                                                             IWriteOnlyList<Animal> fishes.Add(new Fish("Guppy")); <-- this is now safe 

Here's a few videos that shows the concepts:

  • Covariance and Contravariance - VS2010 C# Part 1 of 3
  • Covariance and Contravariance - VS2010 C# Part 2 of 3
  • Covariance and Contravariance - VS2010 C# Part 3 of 3

Here's an example:

namespace SO2719954 {     class Base { }     class Descendant : Base { }      interface IBibbleOut<out T> { }     interface IBibbleIn<in T> { }      class Program     {         static void Main(string[] args)         {             // We can do this since every Descendant is also a Base             // and there is no chance we can put Base objects into             // the returned object, since T is "out"             // We can not, however, put Base objects into b, since all             // Base objects might not be Descendant.             IBibbleOut<Base> b = GetOutDescendant();              // We can do this since every Descendant is also a Base             // and we can now put Descendant objects into Base             // We can not, however, retrieve Descendant objects out             // of d, since all Base objects might not be Descendant             IBibbleIn<Descendant> d = GetInBase();         }          static IBibbleOut<Descendant> GetOutDescendant()         {             return null;         }          static IBibbleIn<Base> GetInBase()         {             return null;         }     } } 

Without these marks, the following could compile:

public List<Descendant> GetDescendants() ... List<Base> bases = GetDescendants(); bases.Add(new Base()); <-- uh-oh, we try to add a Base to a Descendant 

or this:

public List<Base> GetBases() ... List<Descendant> descendants = GetBases(); <-- uh-oh, we try to treat all Bases                                                as Descendants 
like image 98
Lasse V. Karlsen Avatar answered Oct 13 '22 01:10

Lasse V. Karlsen