1. Introduction

Une des nouveautés de .NET 4.0 est l'introduction de la covariance et contravariance.

Sous ces termes un peu ésotériques se cachent des concepts assez simples en réalité. Je vais donc tenter de démystifier tout ça avec cet article et des exemples simples.

Accrochez-vous, c'est parti !

2. Covariance

La covariance, qu'est-ce ?

Une définition générale pourrait être : conversion du plus large au plus fin. (Traduction littérale de l'article anglais de Wikipédia sur la variance : (http://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science))

Nous voilà bien avancés ! Si on tente de traduire cela en langage de développeur : conversion du plus dérivé vers le moins dérivé. Mais encore. Un exemple sera plus clair.

2-A. Exemple de covariance

Ce code :

 
Sélectionnez
IEnumerable<object> test = new List<string>();

Ne compilera pas sous .NET 3.5 et donnera ce message d'erreur : « Cannotimplicitlyconvert type 'System.Collections.Generic.List<string>' to 'System.Collections.Generic.IEnumerable<object>'. An explicit conversion exists (are youmissing a cast?) »

Pourtant List<T> implémente bien IEnumerable<T>, et string est bien dérivé d'object. Mais comme IEnumerable<T> n'est pas covariant, un IEnumerable <string> ne peut être considéré comme un IEnumerable<object> ! (cf. définition : du plus dérivé vers le moins dérivé).

Par contre, à partir de 4.0, pas de problème car IEnumerable<T> est covariant : le paramètre de sortie de type T peut être considéré comme un type U si T dérive de U.

Tentons une définition de la covariance : compatibilité d'assignation, pour une interface ou un délégué ayant un ou plusieurs paramètres génériques de sortie(voir plus bas) de type T1,T2... avec le même interface ou délégué ayant les paramètres génériques de sortie U1,U2... si T1,T2... dérivent de U1,U2...

Toutes les interfaces ne sont pas covariantes en .NET 4.0. Voici la liste de la liste de celles qui le sont :

Comme on le voit, la covariance ne concerne pas que les interfaces. Les délégués peuvent aussi être covariants :

 
Sélectionnez
public Func<string> stringfunc;
Func<object> objectfunc = stringfunc;

Comme vu dans cet exemple, je peux utiliser une fonction qui renvoie T comme une fonction qui renvoie U si T est dérivé de U.

2-B. Création d'interfaces et de délégués covariants

Pour rendre une interface ou un délégué covariant, il faut ajouter le mot-clé out (http://msdn.microsoft.com/en-us/library/dd469487.aspx) au type paramètre concerné. Comme le mot-clé l'indique, celui-ci ne peut alors se trouver uniquement en paramètre de sortie.

2-B-1. Exemple d'interface covariant

 
Sélectionnez
    public interface IFruitTree<out T>
   {
       T GetFruit();
   }


Des classes implémentant cette interface :

 
Sélectionnez
    public class Tree { }
   public class Fruit { }
   public class Orange : Fruit { }
   public class OrangeTree : Tree, IFruitTree<Orange>
   {
       public Orange GetFruit()
       {
           return new Orange();
       }
   }

Et cette fonction :

 
Sélectionnez
public void CatchFruit(IFruitTree<Fruit> fruittree)
{
           ///
}

IFruitTree<out T> étant covariant sur T, nous pouvons passer, au lieu d'un objet implémentant IFruitTree<Fruit>, un objet implémentant un type plus dérivé, comme IFruitTree<Orange> :

 
Sélectionnez
var o = new OrangeTree();
CatchFruit(o);

Comme on le voit, cela simplifie les choses et permet une plus grande réutilisation des composants.

2-B-2. Exemple de délégué covariant

 
Sélectionnez
public delegate T CovariantDelegate<out T>();
void ShowCovariance()
{
   var stringdelegate = new CovariantDelegate<string>(ReturnString);
   CovariantDelegate<object> objectdelegate = stringdelegate;
   object result = objectdelegate.Invoke();
 }
public string ReturnString()
{
   return "Covariance";
}

Comme déjà dit, un paramètre en out ne peut être qu'un paramètre de sortie. Par exemple :

 
Sélectionnez
public interface IsInhabited<out T>
{   
   T DoSomething(T param);
}

Donnera une erreur de compilation :

Invalid variance: The type parameter 'T' must be contravariantly valid on ' Co_Contravariant.IsInhabited <T>. DoSomething ( T)'. 'T' is covariant.

Pourquoi uniquement en paramètre de sortie ? Pour tenter d'expliquer simplement, si T est uniquement en sortie, cela ne pose pas de problème, car il n'y a pas de problème d'assigner à un type T (object, par exemple), le résultat d'une méthode qui renvoie un type U plus dérivé (string, par exemple).

Par contre, si on acceptait <T> en entrée, on pourrait dès lors se retrouver dans un cas comme celui-ci :

 
Sélectionnez
public Func<Exception, int> OKFunc;
public Func<object, int> ProblemsAhead;
void Main()
{
 OKFunc = CountStackTrace;
 ProblemsAhead = OKFunc;
 ...
 var something = ProblemsAhead(new object());
}
public int CountStackTrace(Exception e)
{
 return e.StackTrace.Count();
}

3. Contravariance

Le pendant de la covariance est la contravariance. Pour reprendre le même type de définition : conversion du plus fin au plus large. Traduction : du moins dérivé au plus dérivé.

Ça a l'air moins naturel à première vue, mais c'est tout à fait logique comme on va le constater :

3-A. Exemple de contravariance

Imaginons que nous ayons ceci :

 
Sélectionnez
    public class Animal {}
   public class Dog : Animal {}

Ainsi qu'un comparateur entre Animal qui implémente IEqualityComparer<T> (qui est contravariant) :

 
Sélectionnez
public class CompareAnimals : IEqualityComparer<Animal>
{
 public bool Equals(Animal x, Animal y)
 { 
   . 
 }
 public int GetHashCode(Animal obj)
 {
   .
 }
}

Si nous avons une liste de Dog, ceci est valide :

 
Sélectionnez
var dogs = new List<Dog>();
var distinct = dogs.Distinct(new CompareAnimals());

Alors que la signature de Distinct dans ce cas est : Distinct<Dog>(IEqualityComparer<Dog> Comparer).

Nous passons donc un type moins dérivé (IEqualityComparer<Animal>) à la place d'un plus dérivé (IEqualityComparer<Dog>). Ça peut paraitre paradoxal mais revenons à une définition plus affinée : conversion des paramètres d'entrées du moins dérivé vers le plus dérivé.

En effet : la covariance est uniquement sur les paramètres de sortie et la contravariance est uniquement sur les paramètres d'entrées. Distinct va appeler la méthode Equal(Animal x, Animal y) en lui passant deux instances de Dog alors qu'elle attend deux instances d'Animal. Mais, vu que Dog dérive d'Animal, ça ne pose pas de problème ! Ce qui semble contre nature est en fait tout à fait logique.

Tentons maintenant une définition de la contravariance : compatibilité d'assignation, pour un interface ou un délégué ayant un ou plusieurs paramètres génériques d'entréede type T1,T2... avec le même interface ou délégué ayant les paramètres génériques d'entrée U1,U2... si U1,U2... dérivent de T1,T2...


Voilà la liste des interfaces contravariants de .NET 4.0 :

  • IComparer<in T>(T est contravariant) ;
  • IEqualityComparer<in T>(T est contravariant) ;
  • IComparable<in T>(T est contravariant) ;
  • tous les délégués Action<in T>, Action<in T1...>(T, T1... sont contravariants) ;
  • tous les délégués Func<in T1..., outTResult>(T, T1... sont contravariants).

Comme pour la covariance, les délégués peuvent aussi être contravariants :

 
Sélectionnez
public Action<object> OKAction;
void ShowCovariance()
{
 OKAction = new Action<object>((o) => o.ToString());
 DoSomethingWithAction(OKAction);
} 
static void DoSomethingWithAction(Action<string> action)
{
 action("Contravariance");
}

3-B. Création d'interfaces et de délégués contravariants

Pour créer une interface ou un délégué contravariant, il faut utiliser le mot-clé in (http://msdn.microsoft.com/en-us/library/dd469484.aspx). Comme le nom l'indique aussi, les paramètres en in ne peuvent être que des paramètres d'entrées.

3-B-1. Exemple d'interface contravariant

 
Sélectionnez
public interface ICompareWeigth<in T> where T:Animal
{
  int Weight { get; set; }

  bool SameWeight(T animal);
}

public class CompareAnimalWeight : ICompareWeigth<Animal>
{
  public int CompareWeight (Animal animal1, Animal animal2) 
  {
    // do something;
  }
}

Si on a cette fonction :

 
Sélectionnez
public int CompareDogWeight(ICompareWeigth<Dog> comparison, Dog dog1, Dog dog2)
{
  return comparison.CompareWeight(dog1,dog2);
}

Ceci est valide :

 
Sélectionnez
CompareDogWeight(new CompareAnimalWeight(), new Dog(), new Dog());

3-B-2. Exemple de délégué contravariant

 
Sélectionnez
public delegate void ContravariantDelegate<in T>(T param);
public void DoThing(object o){}
public void TestContravariance(ContravariantDelegate<string> del)
{
 del.Invoke("Contravariance");
}
void ShowContravariance()
{
 ContravariantDelegate<object> OKDelegate = DoThing;
 TestContravariance(OKDelegate);
}

4. Remarques

  • La covariance et contravariance ne concernent que les paramètres de type générique des délégués (depuis .NET 2.0) et des interfaces ainsi que les types tableaux ([]).

Notons, que, pour les types tableaux, c'est à déconseiller car on peut alors arriver à ce genre de choses :

 
Sélectionnez
string[] stringarray = new[]{"Quelle est la réponse ?"};
object[] objectarray = stringarray;
objectarray[0] = 42;

Qui compilera bien, mais il y aura une ArrayTypeMismatchException à l'exécution.

  • La variance dans les génériques n'est supportée que par les types référence, et pas les types valeurs :
 
Sélectionnez
IEnumerable<int> intlist = new List<int>();
IEnumerable<object> objectlist = intlist; // Ne compile pas
  • Le support de la variance n'est implémenté qu'à partir de la version 5 de Silverlight.
  • Le support de la variance n'est pas implémenté sous Windows Phone 7.
  • Il n'est pas possible d'avoir un paramètre à la fois covariant et contravariant. Par contre, il est possible d'avoir un (ou plusieurs) paramètre(s) covariant(s), et un (ou plusieurs) contravariant(s). Dans le framework, les classes Func<in T... ,out TResult> le sont. Un exemple :
 
Sélectionnez
Func<object, string> func = (o) => o.ToString();
Func<string, object> CoContra = func;

5. Conclusion

Voilà, j'espère avoir démystifié ces deux noms barbares.

Le fait de rendre ses interfaces ou délégués covariants et/ou contravariants permet de les réutiliser plus facilement, je suis donc du même avis que Jeffrey Richter, qui recommande donc d'utiliser in et out quand c'est possible afin de permettre une réutilisation dans plus de scénarios.

6. Remerciements

Je tiens à remercier Philippe Vialatte et toute l'équipe de Développez pour leur aide durant la rédaction de cet article.

Un grand merci à Claude Leloup et Maxime Gault pour la correction orthographique.