Retour sur la variance dans les interfaces génériques en .NET

La variance dans les interfaces génériques, n’est pas une nouveauté. Cette fonctionnalité existe depuis le Framework 4.0. La variance permet une plus grande flexibilité pour l’assignation et l’utilisation des types génériques.
Elle reste néanmoins peu utilisée dans le code « utilisateur » alors que les interfaces exposées par le Framework .NET l’utilisent énormément.

Lors de notre dernière session des TechEvenings consacrée aux « Reactive Extensions », des questions concernant la variance des interfaces IObservable et IObserver furent posées. Ce post est donc l’occasion de revenir plus en détail sur cette notion.

En programmation objet, il est courant d’écrire le code suivant :

class Fruit { }
class Pomme : Fruit { }

Fruit pomme = new Pomme();

Or si l’on résonne sur des ensembles, on aimerait également pouvoir écrire le code suivant :

IEnumerable<Fruit> pommes = new List<Pomme>();

Or, ce code provoque une erreur si compilé avant le Framework 4.0. En effet, cette conversion implicite est impossible. Voici la définition de la classe List et de l’interface IEnumerable :

public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>
public interface IEnumerable<T> : IEnumerable

Avec le framework 4.0, ce code compile grâce à de nouveaux mots clefs out et in permettant d’autoriser respectivement la covariance et la contravariance, comme nous allons le voir.

L’interface IEnumerable<T> est désormais la suivante en Framework 4.0 :

public interface IEnumerable<out T> : IEnumerable

Attention,

  • La variance dans les interfaces génériques est possible uniquement pour les types référence. (Donc pas les types valeur comme int, les énumérations ou les structures).
  • Les classes qui implémentent des interfaces variantes sont toujours invariantes.

Il est donc impossible d’écrire ceci :

List<Fruit> pommes = new List<Pomme>();

En revanche, en passant par l’interface, pas de problèmes

IEnumerable<Fruit> pommes = new List<Pomme>();

Covariance

La covariance est donc mise en œuvre par le mot clef out. C’est la notion la plus intuitive car elle est proche de celle du polymorphisme que nous utilisons tous les jours en POO. Dans notre exemple précédent et en langage naturel, il paraît normal de pouvoir ranger des pommes dans un panier conçu pour stocker des fruits (notre IEnumerable<Fruit>).

Les interfaces IEnumerable<T>, IQueryable<T>, IObservable<T> sont covariantes.

Contravariance

Sa mise en œuvre passe logiquement par le mot clef in.

Les interfaces IComparable<T>, IComparer<T>, IObserver<T> sont contravariantes.

En langage naturel, il est possible de se dire qu'en étant capable de comparer deux fruits, on est capable de comparer deux pommes, ce qui se traduit par le code suivant :

class Fruit : IComparer<Fruit>
{
    public int Compare(Fruit x, Fruit y)
    {
        // code de comparaison
    }
}

class Pomme : Fruit { }

IComparer<Pomme> pommeComparer = new Fruit();

En synthèse

Pour une interface, les types covariants peuvent être utilisés comme types de retour des méthodes de l'interface et les types contravariants peuvent être utilisés comme types de paramètres des méthodes de l'interface.
Cette approche permet une flexibilité d’utilisation bien appréciable dans le cadre des manipulations courantes des interfaces génériques.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.