web-dev-qa-db-fra.com

Comprendre les interfaces Covariant et Contravariant en C #

Je les ai rencontrés dans un manuel que je lis sur C #, mais j'ai du mal à les comprendre, probablement à cause du manque de contexte.

Y a-t-il une bonne explication concise de ce qu'ils sont et de leur utilité?

Modifier pour clarification:

Interface covariante:

interface IBibble<out T>
.
.

Interface Contravariante:

interface IBibble<in T>
.
.
81
NibblyPig

Avec <out T>, vous pouvez traiter la référence d'interface comme une référence vers le haut dans la hiérarchie.

Avec <in T>, vous pouvez traiter la référence d'interface comme une référence vers le bas dans la recherche.

Permettez-moi d'essayer de l'expliquer en termes plus anglais.

Disons que vous récupérez une liste d'animaux de votre zoo et que vous avez l'intention de les traiter. Tous les animaux (dans votre zoo) ont un nom et un identifiant unique. Certains animaux sont des mammifères, certains sont des reptiles, certains sont des amphibiens, certains sont des poissons, etc. mais ce sont tous des animaux.

Donc, avec votre liste d'animaux (qui contient des animaux de différents types), vous pouvez dire que tous les animaux ont un nom, donc évidemment il serait sûr d'obtenir le nom de tous les animaux.

Cependant, que se passe-t-il si vous n'avez qu'une liste de poissons, mais que vous devez les traiter comme des animaux, cela fonctionne-t-il? Intuitivement, cela devrait fonctionner, mais en C # 3.0 et avant, ce morceau de code ne compilera pas:

IEnumerable<Animal> animals = GetFishes(); // returns IEnumerable<Fish>

La raison en est que le compilateur ne "sait" pas ce que vous avez l'intention, ou peut , faire avec la collection d'animaux après l'avoir récupérée . Pour autant qu'il sache, il pourrait y avoir un chemin à travers IEnumerable<T> pour remettre un objet dans la liste, et cela vous permettrait potentiellement de mettre un animal qui n'est pas un poisson, dans une collection qui ne devrait contenir que du poisson.

En d'autres termes, le compilateur ne peut garantir que cela n'est pas autorisé:

animals.Add(new Mammal("Zebra"));

Le compilateur refuse donc carrément de compiler votre code. C'est la covariance.

Regardons la contravariance.

Étant donné que notre zoo peut gérer tous les animaux, il peut certainement gérer les poissons, alors essayons d'ajouter du poisson à notre zoo.

En C # 3.0 et avant, cela ne compile pas:

List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal>
fishes.Add(new Fish("Guppy"));

Ici, le compilateur pourrait autoriser ce morceau de code, même si la méthode renvoie List<Animal> simplement parce que tous les poissons sont des animaux, donc si nous venons de changer les types en ceci:

List<Animal> fishes = GetAccessToFishes();
fishes.Add(new Fish("Guppy"));

Ensuite, cela fonctionnerait, mais le compilateur ne peut pas déterminer que vous n'essayez pas de faire ceci:

List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal>
Fish firstFist = fishes[0];

Étant donné que la liste est en fait une liste d'animaux, cela n'est pas autorisé.

La contre-et la co-variance sont donc la façon dont vous traitez les références d'objet et ce que vous êtes autorisé à en faire.

Les mots clés in et out dans C # 4.0 marquent spécifiquement l'interface comme l'un ou l'autre. Avec in, vous êtes autorisé à placer le type générique (généralement T) dans entrée - positions, ce qui signifie des arguments de méthode, et propriétés en écriture seule.

Avec out, vous êtes autorisé à placer le type générique dans sortie - positions, qui est des valeurs de retour de méthode, propriétés en lecture seule et les paramètres de méthode out.

Cela vous permettra de faire ce que vous vouliez faire avec le code:

IEnumerable<Animal> animals = GetFishes(); // returns IEnumerable<Fish>
// since we can only get animals *out* of the collection, every fish is an animal
// so this is safe

List<T> a à la fois des directions entrantes et sortantes sur T, donc ce n'est ni co-variante ni contre-variante, mais une interface qui vous a permis d'ajouter des objets, comme ceci:

interface IWriteOnlyList<in T>
{
    void Add(T value);
}

vous permettrait de faire ceci:

IWriteOnlyList<Fish> fishes = GetWriteAccessToAnimals(); // still returns
                                                            IWriteOnlyList<Animal>
fishes.Add(new Fish("Guppy")); <-- this is now safe

Voici quelques vidéos qui montrent les concepts:

Voici un exemple:

namespace SO2719954
{
    class Base { }
    class Descendant : Base { }

    interface IBibbleOut<out T> { }
    interface IBibbleIn<in T> { }

    class Program
    {
        static void Main(string[] args)
        {
            // We can do this since every Descendant is also a Base
            // and there is no chance we can put Base objects into
            // the returned object, since T is "out"
            // We can not, however, put Base objects into b, since all
            // Base objects might not be Descendant.
            IBibbleOut<Base> b = GetOutDescendant();

            // We can do this since every Descendant is also a Base
            // and we can now put Descendant objects into Base
            // We can not, however, retrieve Descendant objects out
            // of d, since all Base objects might not be Descendant
            IBibbleIn<Descendant> d = GetInBase();
        }

        static IBibbleOut<Descendant> GetOutDescendant()
        {
            return null;
        }

        static IBibbleIn<Base> GetInBase()
        {
            return null;
        }
    }
}

Sans ces marques, les éléments suivants pourraient être compilés:

public List<Descendant> GetDescendants() ...
List<Base> bases = GetDescendants();
bases.Add(new Base()); <-- uh-oh, we try to add a Base to a Descendant

ou ca:

public List<Base> GetBases() ...
List<Descendant> descendants = GetBases(); <-- uh-oh, we try to treat all Bases
                                               as Descendants
134

Cet article est le meilleur que j'ai lu sur le sujet

En bref, la covariance/contravariance/invariance traite de la conversion automatique de type (de la base au dérivé et vice-versa). Ces conversions de type ne sont possibles que si certaines garanties sont respectées en termes d'actions de lecture/écriture effectuées sur les objets castés. Lisez l'article pour plus de détails.

6
Ben G