Table of Contents
Before making the video, I checked for a Spanish term, and it does not exist; only the English version exists, so we will continue with that.
The first thing we're going to see is the object structure, or the model we will need to understand the post, since it can be quite complex without a clear structure:
public class Persona
{
public string Name { get;set;}
}
public class Empleado : Persona
{
public int Id { get; set; }
}
public class Jefe : Empleado
{
}
public class Becario : Empleado
{
}
This is the resulting entity relationship model:
As you can see, you need to understand object-oriented programming very well, especially inheritance. If you need clarification, here is a link.
Our specific case is also closely related to generic types, for which there is also a post.
1 - Invariance
Before we explain covariance and contravariance, let's talk about the default case.
As we recall from the generics post, elements like List
use T
as a generic type, which means they do not have the in
or out
keywords or decorators.
As we can see in this example, we create a method that accepts a List
of Persona
and prints their types:
public static void ImprimirPersonas(List<Persona> personas)
{
foreach (var persona in personas)
{
Console.WriteLine($"El elemento actual es de tipo {persona.GetType().ToString()}");
}
}
And this code will accept all entities that inherit from Persona
:
var listaPersonas = new List<Persona>()
{
new Becario(),
new Jefe()
};
Utils.ImprimirPersonas(listaPersonas);
However, passing a collection that is a child of our original Persona
type gives us an error:
var listaBecarios = new List<Becario>()
{
new Becario(),
new Becario()
};
Utils.ImprimirPersonas(listaBecarios);
And the error says it cannot convert from List<Persona>
to List<Becario>
.
So far so good, we've seen this before. But why?
The reason is that the generic type of List<T>
is not covariant. As mentioned, List<T>
is invariant, which means it only accepts collections where the T
type is Persona
.
The reason the compiler does this is to prevent us from doing this:
public static void ImprimirPersonas(List<Persona> personas)
{
personas.Add(new Becario());
}
As you can see, it's the same method signature, but its content is completely wrong. We are adding an element to the list, which could violate other constraint conditions for the methods or types. Remember that we discussed how to restrict conditions in the post about generic types.
2 - Covariance
We use covariance
mainly when we create immutable collections. With these collections, we prevent adding or removing elements.
To indicate a type is covariant, we must add the out
keyword to the generic type.
In this example, I will use IEnumerable<out T>
, which is a built-in .NET type very similar to lists, but includes the out
keyword.
So, let's create the following method:
public static void ImprimirPersonas(IEnumerable<Persona> personas)
{
foreach (var persona in personas)
{
Console.WriteLine($"El elemento actual es de tipo {persona.GetType().ToString()}");
}
}
You can see it's almost identical to the previous one, we just change the type we pass to the method.
Since it's a type that allows generic and covariant
, it means we can pass a list of any type that inherits from our main list type.
In this particular case, we can pass a list where the generic type could be Persona
, Becario
, Empleado
, or Jefe
.
If we go back to the previous example:
var listaBecarios = new List<Becario>()
{
new Becario(),
new Becario()
};
Utils.ImprimirPersonas(listaBecarios);
You can see it no longer gives us an error.
3 - Contravariance
We commonly use contravariance when passing functions as parameters, and we must indicate the generic type with the in
keyword. In C#, a built-in type that accepts in
is Action<in T>
, which is a delegate.
We can create a method that receives an Action<Becario>
as a parameter and invokes it with a new instance of Becario
.
public static void RealizarActionBecario(Action<Becario> becarioAction)
{
Becario becario = new Becario();
becarioAction(becario);
}
We write the code to call this method, and it works correctly:
var accionBecario = new Action<Becario>(z => Console.WriteLine("Preparo el café"));
Utils.RealizarActionBecario(accionBecario);
However, the following code fails:
var accionJefe = new Action<Jefe>(z => Console.WriteLine("Organizo un meeting"));
Utils.RealizarActionBecario(accionJefe);
And that's because it can't convert from Action<Becario>
to Action<Jefe>
.
But here's where our in
decorator comes into play. If we run the following code:
var accionEmpleado = new Action<Empleado>(z => Console.WriteLine("trabajo duro"));
Utils.RealizarActionBecario(accionEmpleado);
It works perfectly, and this is because by using the reserved word in
, we allow its class and its base classes, in this case Empleado
and Persona
, to work correctly.
Conclusion
- The use of Covariance and Contravariance is highly linked to generic types.
- Invariance: only allows the specified type, with the reserved word in.
- Covariance: reserved word
out
; allows a type or its derived types. - Contravariance: reserved word
in
; allows a type or its implementing types, or parent types. - Understanding how this works is important.
- It helps us write more robust and structured code.
If there is any problem you can add a comment bellow or contact me in the website's contact form