covariance et contravariance en .net : heritage avec des types generiques
Récemment, j'ai été confronté à un code du genre :
namespace ConsoleApplication60
{
class Program
{
static void Main(string[] args)
{
List<Person> persons = new List<Person>();
// peuplement de la liste de person
DoAction(persons);
}
/// <param name="persons">
/// Utilisation d'un IEnumerable<IPerson> plutot qu'une List<Person> pour être le plus
/// générique possible et ainsi pouvoir lui passer un Person[] voir meme un Customer[]
/// </param>
static void DoAction(IEnumerable<IPerson> persons)
{
foreach (IPerson person in persons)
{
}
}
}
public class Person : IPerson
{
public string FirstName
{
get { return "cyril"; }
}
}
public interface IPerson
{
String FirstName { get; }
}
}
Avec ce code j'obtiens une erreur de compilation :
Argument '1': cannot convert from 'System.Collections.Generic.List<ConsoleApplication60.Person>' to 'System.Collections.Generic.IEnumerable<ConsoleApplication60.IPerson>'
Mais pourquoi n'arrive t'il pas à convertir une List<Person> en IEnumerable<IPerson> ? pourtant List<T> implémente IEnumerable<T> et Person, IPerson. Tout semble pourtant OK.
Pour bien comprendre le problème j'ai fait une petite recherche et je suis tombé sur ce thread : Casting generic collections & inheritance. En résumé, si on avait mis une List<Person> en argument de la méthode DoAction, le code plus haut ne compilerait pas non plus. Mais s'il compilait, cela voudrait dire que la méthode DoAction prendrait en paramètre une List<IPerson> qui pointe en fait vers une List<Person>. Pourtant rien ne nous empêche d'ajouter un objet de type Customer (qui implémente IPerson mais hérite pas de Person) à notre List<IPerson>. Il y aurait donc un non-sens car une List<Person> pourrais contenir un Customer !
Là où cela devient vraiment intéressant c'est avec ce post : Covariance and Contravariance in .NET, Java and C++. MSIL (Microsoft Intermediate Language) permet le code plus haut. Ce pseudo code C# est possible en MSIL, mais aucun langage ne l'implémente !
class Mammal { }
class Dog : Mammal { }
interface IReader<+T> // allows covariance
{
T GetValue();
}
interface IWriter<-T> // allows contravariance
{
void SetValue(T value);
}
// With these definitions, covariance of generic parameters would allow this:
IReader<Dog> dogReader = null;
IReader<Mammal> mammalReader = dogReader;
// Contravariance of generic parameters would allow this:
IWriter<Mammal> mammalWriter = null;
IWriter<Dog> dogWriter = mammalWriter;
Pour revenir au problème initial il suffit de spécifier le type final en "paramètre" de la méthode. Inutile ensuite de spécifier ce paramètre, l'inférence de type s'en chargera :
static void Main(string[] args)
{
List<Person> persons = new List<Person>();
// peuplement de la liste de person
DoAction(persons);
DoAction(new Person[3]);
}
static void DoAction<T>(IEnumerable<T> persons) where T : IPerson
{
foreach (IPerson person in persons)
{
}
}