Here's a simple example using an inheritance hierarchy.
Given the simple class hierarchy:
And in code:
public abstract class LifeForm { }
public abstract class Animal : LifeForm { }
public class Giraffe : Animal { }
public class Zebra : Animal { }
Invariance (i.e. generic type parameters not decorated with in
or out
keywords)
Seemingly, a method such as this
public static void PrintLifeForms(IList<LifeForm> lifeForms)
{
foreach (var lifeForm in lifeForms)
{
Console.WriteLine(lifeForm.GetType().ToString());
}
}
... should accept a heterogeneous collection: (which it does)
var myAnimals = new List<LifeForm>
{
new Giraffe(),
new Zebra()
};
PrintLifeForms(myAnimals); // Giraffe, Zebra
However, passing a collection of a more derived type fails!
var myGiraffes = new List<Giraffe>
{
new Giraffe(), // "Jerry"
new Giraffe() // "Melman"
};
PrintLifeForms(myGiraffes); // Compile Error!
cannot convert from 'System.Collections.Generic.List<Giraffe>' to 'System.Collections.Generic.IList<LifeForm>'
Why? Because the generic parameter IList<LifeForm>
is not covariant -
IList<T>
is invariant, so IList<LifeForm>
only accepts collections (which implement IList) where the parameterized type T
must be LifeForm
.
If the method implementation of PrintLifeForms
was malicious (but has same method signature), the reason why the compiler prevents passing List<Giraffe>
becomes obvious:
public static void PrintLifeForms(IList<LifeForm> lifeForms)
{
lifeForms.Add(new Zebra());
}
Since IList
permits adding or removal of elements, any subclass of LifeForm
could thus be added to the parameter lifeForms
, and would violate the type of any collection of derived types passed to the method. (Here, the malicious method would attempt to add a Zebra
to var myGiraffes
). Fortunately, the compiler protects us from this danger.
Covariance (Generic with parameterized type decorated with out
)
Covariance is widely used with immutable collections (i.e. where new elements cannot be added or removed from a collection)
The solution to the example above is to ensure that a covariant generic collection type is used, e.g. IEnumerable
(defined as IEnumerable<out T>
). IEnumerable
has no methods to change to the collection, and as a result of the out
covariance, any collection with subtype of LifeForm
may now be passed to the method:
public static void PrintLifeForms(IEnumerable<LifeForm> lifeForms)
{
foreach (var lifeForm in lifeForms)
{
Console.WriteLine(lifeForm.GetType().ToString());
}
}
PrintLifeForms
can now be called with Zebras
, Giraffes
and any IEnumerable<>
of any subclass of LifeForm
.
var myGiraffes = new List<Giraffe>
{
new Giraffe(), // "Jerry"
new Giraffe() // "Melman"
};
PrintLifeForms(myGiraffes); // All good!
Contravariance (Generic with parameterized type decorated with in
)
Contravariance is frequently used when functions are passed as parameters.
Here's an example of a function, which takes an Action<Zebra>
as a parameter, and invokes it on a known instance of a Zebra:
public void PerformZebraAction(Action<Zebra> zebraAction)
{
var zebra = new Zebra();
zebraAction(zebra);
}
As expected, this works just fine:
var myAction = new Action<Zebra>(z => Console.WriteLine("I'm a zebra"));
PerformZebraAction(myAction); // I'm a zebra
Intuitively, this will fail:
var myAction = new Action<Giraffe>(g => Console.WriteLine("I'm a giraffe"));
PerformZebraAction(myAction);
cannot convert from 'System.Action<Giraffe>' to 'System.Action<Zebra>'
However, this succeeds
var myAction = new Action<Animal>(a => Console.WriteLine("I'm an animal"));
PerformZebraAction(myAction); // I'm an animal
and even this also succeeds:
var myAction = new Action<object>(a => Console.WriteLine("I'm an amoeba"));
PerformZebraAction(myAction); // I'm an amoeba
Why? Because Action
is defined as Action<in T>
, i.e. it is contravariant
, meaning that for Action<Zebra> myAction
, that myAction
can be at "most" a Action<Zebra>
, but less derived superclasses of Zebra
are also acceptable.
Although this may be non-intuitive at first (e.g. how can an Action<object>
be passed as a parameter requiring Action<Zebra>
?), if you unpack the steps, you will note that the called function (PerformZebraAction
) itself is responsible for passing data (in this case a Zebra
instance) to the function - the data doesn't come from the calling code.
Because of the inverted approach of using higher order functions in this manner, by the time the Action
is invoked, it is the more derived Zebra
instance which is invoked against the zebraAction
function (passed as a parameter), although the function itself uses a less derived type.