Exemple de covariance et de contravariance dans le monde réel

162

J'ai un peu de mal à comprendre comment j'utiliserais la covariance et la contravariance dans le monde réel.

Jusqu'à présent, les seuls exemples que j'ai vus sont le même ancien exemple de tableau.

object[] objectArray = new string[] { "string 1", "string 2" };

Ce serait bien de voir un exemple qui me permettrait de l'utiliser lors de mon développement si je pouvais le voir être utilisé ailleurs.

Le rasoir
la source
1
J'explore covariance dans cette réponse à (mes) question: types de covariance: par exemple . Je pense que vous le trouverez intéressant et, espérons-le, instructif.
Cristian Diaconescu

Réponses:

109

Disons que vous avez une classe Personne et une classe qui en dérive, Maître. Vous avez des opérations qui prennent un IEnumerable<Person>comme argument. Dans votre classe School, vous avez une méthode qui renvoie un fichier IEnumerable<Teacher>. La covariance vous permet d'utiliser directement ce résultat pour les méthodes qui prennent un IEnumerable<Person>, en remplaçant un type plus dérivé par un type moins dérivé (plus générique). Contravariance, contre-intuitivement, vous permet d'utiliser un type plus générique, où un type plus dérivé est spécifié.

Voir aussi Covariance et Contravariance dans les génériques sur MSDN .

Classes :

public class Person 
{
     public string Name { get; set; }
} 

public class Teacher : Person { } 

public class MailingList
{
    public void Add(IEnumerable<out Person> people) { ... }
}

public class School
{
    public IEnumerable<Teacher> GetTeachers() { ... }
}

public class PersonNameComparer : IComparer<Person>
{
    public int Compare(Person a, Person b) 
    { 
        if (a == null) return b == null ? 0 : -1;
        return b == null ? 1 : Compare(a,b);
    }

    private int Compare(string a, string b)
    {
        if (a == null) return b == null ? 0 : -1;
        return b == null ? 1 : a.CompareTo(b);
    }
}

Utilisation :

var teachers = school.GetTeachers();
var mailingList = new MailingList();

// Add() is covariant, we can use a more derived type
mailingList.Add(teachers);

// the Set<T> constructor uses a contravariant interface, IComparer<in T>,
// we can use a more generic type than required.
// See https://msdn.microsoft.com/en-us/library/8ehhxeaf.aspx for declaration syntax
var teacherSet = new SortedSet<Teachers>(teachers, new PersonNameComparer());
Tvanfosson
la source
14
@FilipBartuzi - si, comme moi quand j'ai écrit cette réponse, vous étiez employé dans une université qui est vraiment un exemple du monde réel.
tvanfosson
5
Comment cela peut-il être marqué comme réponse quand il ne répond pas à la question et ne donne aucun exemple d'utilisation de la variance co / contra dans c #?
barakcaf
@barakcaf a ajouté un exemple de contravariance. Je ne sais pas pourquoi vous ne voyiez pas l'exemple de covariance - peut-être que vous deviez faire défiler le code vers le bas - mais j'ai ajouté quelques commentaires à ce sujet.
tvanfosson
@tvanfosson le code utilise co / contra, je ment qu'il ne montre pas comment le déclarer. L'exemple ne montre pas l'utilisation de in / out dans la déclaration générique alors que l'autre réponse le fait.
barakcaf
Donc, si je comprends bien, la covariance est ce qui permet le principe de substitution de Liskov en C #, est-ce vrai?
Miguel Veloso
136
// Contravariance
interface IGobbler<in T> {
    void gobble(T t);
}

// Since a QuadrupedGobbler can gobble any four-footed
// creature, it is OK to treat it as a donkey gobbler.
IGobbler<Donkey> dg = new QuadrupedGobbler();
dg.gobble(MyDonkey());

// Covariance
interface ISpewer<out T> {
    T spew();
}

// A MouseSpewer obviously spews rodents (all mice are
// rodents), so we can treat it as a rodent spewer.
ISpewer<Rodent> rs = new MouseSpewer();
Rodent r = rs.spew();

Par souci d'exhaustivité…

// Invariance
interface IHat<T> {
    void hide(T t);
    T pull();
}

// A RabbitHat…
IHat<Rabbit> rHat = RabbitHat();

// …cannot be treated covariantly as a mammal hat…
IHat<Mammal> mHat = rHat;      // Compiler error
// …because…
mHat.hide(new Dolphin());      // Hide a dolphin in a rabbit hat??

// It also cannot be treated contravariantly as a cottontail hat…
IHat<CottonTail> cHat = rHat;  // Compiler error
// …because…
rHat.hide(new MarshRabbit());
cHat.pull();                   // Pull a marsh rabbit out of a cottontail hat??
Marcelo Cantos
la source
138
J'aime cet exemple réaliste. J'écrivais juste du code pour engloutir un âne la semaine dernière et j'étais si heureux que nous ayons la covariance maintenant. :-)
Eric Lippert
4
Ce commentaire ci-dessus avec @javadba disant à THE EricLippert ce qu'est la covariance et la contravariance est un exemple de covariance réaliste de moi disant à ma grand-mère comment sucer des œufs! : p
iAteABug_And_iLiked_it
1
La question ne demandait pas ce que la contravariance et la covariance pouvaient faire , elle demandait pourquoi vous auriez besoin de l'utiliser . Votre exemple est loin d'être pratique car il n'exige ni l'un ni l'autre. Je peux créer un QuadrupedGobbler et le traiter comme lui-même (attribuez-le à IGobbler <Quadruped>) et il peut toujours engloutir des ânes (je peux passer un Donkey à la méthode Gobble qui nécessite un Quadruped). Aucune contravariance nécessaire. C'est cool que nous puissions traiter un QuadrupedGobbler comme un DonkeyGobbler, mais pourquoi aurions-nous besoin, dans ce cas, si un QuadrupedGobbler peut déjà engloutir des Donkeys?
wired_in
1
@wired_in Parce que lorsque vous ne vous souciez que des ânes, être plus général peut vous gêner. Par exemple, si vous avez une ferme qui fournit des ânes à engloutir, vous pouvez exprimer cela par void feed(IGobbler<Donkey> dg). Si vous preniez plutôt un IGobbler <Quadruped> comme paramètre, vous ne pourriez pas passer un dragon qui ne mange que des ânes.
Marcelo Cantos
1
Waaay en retard à la fête, mais c'est à peu près le meilleur exemple écrit que j'ai vu autour de SO. Cela a un sens tout en étant ridicule. Je vais devoir améliorer mon jeu avec des réponses ...
Jesse Williams
122

Voici ce que j'ai mis en place pour m'aider à comprendre la différence

public interface ICovariant<out T> { }
public interface IContravariant<in T> { }

public class Covariant<T> : ICovariant<T> { }
public class Contravariant<T> : IContravariant<T> { }

public class Fruit { }
public class Apple : Fruit { }

public class TheInsAndOuts
{
    public void Covariance()
    {
        ICovariant<Fruit> fruit = new Covariant<Fruit>();
        ICovariant<Apple> apple = new Covariant<Apple>();

        Covariant(fruit);
        Covariant(apple); //apple is being upcasted to fruit, without the out keyword this will not compile
    }

    public void Contravariance()
    {
        IContravariant<Fruit> fruit = new Contravariant<Fruit>();
        IContravariant<Apple> apple = new Contravariant<Apple>();

        Contravariant(fruit); //fruit is being downcasted to apple, without the in keyword this will not compile
        Contravariant(apple);
    }

    public void Covariant(ICovariant<Fruit> fruit) { }

    public void Contravariant(IContravariant<Apple> apple) { }
}

tldr

ICovariant<Fruit> apple = new Covariant<Apple>(); //because it's covariant
IContravariant<Apple> fruit = new Contravariant<Fruit>(); //because it's contravariant
CSharper
la source
10
C'est la meilleure chose que j'ai vue jusqu'à présent qui soit claire et concise. Excellent exemple!
Rob L
6
Comment le fruit peut-il être abattu en pomme (dans l' Contravarianceexemple) quand Fruitest le parent de Apple?
Tobias Marschall
@TobiasMarschall cela signifie que vous devez étudier davantage le "polymorphisme"
snr
56

Les mots clés in et out contrôlent les règles de conversion du compilateur pour les interfaces et les délégués avec des paramètres génériques:

interface IInvariant<T> {
    // This interface can not be implicitly cast AT ALL
    // Used for non-readonly collections
    IList<T> GetList { get; }
    // Used when T is used as both argument *and* return type
    T Method(T argument);
}//interface

interface ICovariant<out T> {
    // This interface can be implicitly cast to LESS DERIVED (upcasting)
    // Used for readonly collections
    IEnumerable<T> GetList { get; }
    // Used when T is used as return type
    T Method();
}//interface

interface IContravariant<in T> {
    // This interface can be implicitly cast to MORE DERIVED (downcasting)
    // Usually means T is used as argument
    void Method(T argument);
}//interface

class Casting {

    IInvariant<Animal> invariantAnimal;
    ICovariant<Animal> covariantAnimal;
    IContravariant<Animal> contravariantAnimal;

    IInvariant<Fish> invariantFish;
    ICovariant<Fish> covariantFish;
    IContravariant<Fish> contravariantFish;

    public void Go() {

        // NOT ALLOWED invariants do *not* allow implicit casting:
        invariantAnimal = invariantFish; 
        invariantFish = invariantAnimal; // NOT ALLOWED

        // ALLOWED covariants *allow* implicit upcasting:
        covariantAnimal = covariantFish; 
        // NOT ALLOWED covariants do *not* allow implicit downcasting:
        covariantFish = covariantAnimal; 

        // NOT ALLOWED contravariants do *not* allow implicit upcasting:
        contravariantAnimal = contravariantFish; 
        // ALLOWED contravariants *allow* implicit downcasting
        contravariantFish = contravariantAnimal; 

    }//method

}//class

// .NET Framework Examples:
public interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable { }
public interface IEnumerable<out T> : IEnumerable { }


class Delegates {

    // When T is used as both "in" (argument) and "out" (return value)
    delegate T Invariant<T>(T argument);

    // When T is used as "out" (return value) only
    delegate T Covariant<out T>();

    // When T is used as "in" (argument) only
    delegate void Contravariant<in T>(T argument);

    // Confusing
    delegate T CovariantBoth<out T>(T argument);

    // Confusing
    delegate T ContravariantBoth<in T>(T argument);

    // From .NET Framework:
    public delegate void Action<in T>(T obj);
    public delegate TResult Func<in T, out TResult>(T arg);

}//class
Jack
la source
En supposant que le poisson est un sous-type d'animal. Excellente réponse au fait.
Rajan Prasad
49

Voici un exemple simple utilisant une hiérarchie d'héritage.

Compte tenu de la hiérarchie de classes simple:

entrez la description de l'image ici

Et en code:

public abstract class LifeForm  { }
public abstract class Animal : LifeForm { }
public class Giraffe : Animal { }
public class Zebra : Animal { }

Invariance (c.-à-d. Paramètres de type générique * non * décorés avec des mots clés inou out)

Apparemment, une méthode comme celle-ci

public static void PrintLifeForms(IList<LifeForm> lifeForms)
{
    foreach (var lifeForm in lifeForms)
    {
        Console.WriteLine(lifeForm.GetType().ToString());
    }
}

... devrait accepter une collection hétérogène: (ce qu'elle fait)

var myAnimals = new List<LifeForm>
{
    new Giraffe(),
    new Zebra()
};
PrintLifeForms(myAnimals); // Giraffe, Zebra

Cependant, le passage d'une collection d'un type plus dérivé échoue!

var myGiraffes = new List<Giraffe>
{
    new Giraffe(), // "Jerry"
    new Giraffe() // "Melman"
};
PrintLifeForms(myGiraffes); // Compile Error!

cannot convert from 'System.Collections.Generic.List<Giraffe>' to 'System.Collections.Generic.IList<LifeForm>'

Pourquoi? Parce que le paramètre générique IList<LifeForm>n'est pas covariant - IList<T>est invariant, donc IList<LifeForm>n'accepte que les collections (qui implémentent IList) où le type paramétré Tdoit être LifeForm.

Si l'implémentation de la méthode de PrintLifeFormsétait malveillante (mais a la même signature de méthode), la raison pour laquelle le compilateur empêche le passage List<Giraffe>devient évidente:

 public static void PrintLifeForms(IList<LifeForm> lifeForms)
 {
     lifeForms.Add(new Zebra());
 }

Puisque IListpermet l'ajout ou la suppression d'éléments, toute sous-classe de LifeFormpourrait donc être ajoutée au paramètre lifeForms, et violerait le type de toute collection de types dérivés passés à la méthode. (Ici, la méthode malveillante tenterait d'ajouter un Zebraà var myGiraffes). Heureusement, le compilateur nous protège de ce danger.

Covariance (générique avec type paramétré décoré avec out)

La covariance est largement utilisée avec les collections immuables (c'est-à-dire lorsque de nouveaux éléments ne peuvent pas être ajoutés ou supprimés d'une collection)

La solution à l'exemple ci-dessus est de s'assurer qu'un type de collection générique covariant est utilisé, par exemple IEnumerable(défini comme IEnumerable<out T>). IEnumerablen'a pas de méthode à modifier dans la collection, et en raison de la outcovariance, toute collection avec le sous-type de LifeFormpeut maintenant être passée à la méthode:

public static void PrintLifeForms(IEnumerable<LifeForm> lifeForms)
{
    foreach (var lifeForm in lifeForms)
    {
        Console.WriteLine(lifeForm.GetType().ToString());
    }
}

PrintLifeFormspeut maintenant être appelé avec Zebras, Giraffeset n'importe IEnumerable<>quelle sous-classe deLifeForm

Contravariance (générique avec type paramétré décoré avec in)

La contravariance est fréquemment utilisée lorsque les fonctions sont passées en paramètres.

Voici un exemple de fonction, qui prend un Action<Zebra>comme paramètre et l'appelle sur une instance connue d'un Zebra:

public void PerformZebraAction(Action<Zebra> zebraAction)
{
    var zebra = new Zebra();
    zebraAction(zebra);
}

Comme prévu, cela fonctionne très bien:

var myAction = new Action<Zebra>(z => Console.WriteLine("I'm a zebra"));
PerformZebraAction(myAction); // I'm a zebra

Intuitivement, cela échouera:

var myAction = new Action<Giraffe>(g => Console.WriteLine("I'm a giraffe"));
PerformZebraAction(myAction); 

cannot convert from 'System.Action<Giraffe>' to 'System.Action<Zebra>'

Cependant, cela réussit

var myAction = new Action<Animal>(a => Console.WriteLine("I'm an animal"));
PerformZebraAction(myAction); // I'm an animal

et même cela réussit aussi:

var myAction = new Action<object>(a => Console.WriteLine("I'm an amoeba"));
PerformZebraAction(myAction); // I'm an amoeba

Pourquoi? Parce que Actionest défini comme Action<in T>, c'est-à-dire qu'il est contravariant, ce qui signifie que pour Action<Zebra> myAction, qui myActionpeut être au plus "a" Action<Zebra>, mais des superclasses moins dérivées de Zebrasont également acceptables.

Bien que cela puisse être non intuitif au début (par exemple, comment Action<object>passer un paramètre en tant que paramètre nécessitant Action<Zebra>?), Si vous décompressez les étapes, vous remarquerez que la fonction appelée ( PerformZebraAction) elle-même est responsable de la transmission des données (dans ce cas, une Zebrainstance ) à la fonction - les données ne proviennent pas du code appelant.

En raison de l'approche inversée consistant à utiliser des fonctions d'ordre supérieur de cette manière, au moment où l ' Actionest invoqué, c'est l' Zebrainstance la plus dérivée qui est invoquée contre la zebraActionfonction (passée en paramètre), bien que la fonction elle-même utilise un type moins dérivé.

StuartLC
la source
7
Ceci est une excellente explication pour les différentes options de variance, car il décrit l'exemple et clarifie également pourquoi le compilateur restreint ou autorise sans les mots
Vikhram
Où est le inmot - clé utilisé pour la contravariance ?
javadba
@javadba dans ce qui précède, Action<in T>et Func<in T, out TResult>sont contravariants dans le type d'entrée. (Mes exemples utilisent des types existants invariant (List), covariant (IEnumerable) et contravariant (Action, Func))
StuartLC
Ok je ne le fais pas C#, je ne le saurais pas.
javadba
C'est assez similaire dans Scala, juste une syntaxe différente - [+ T] serait covariant dans T, [-T] serait contravariant dans T, Scala peut également appliquer la contrainte 'between' et la sous-classe promiscuous 'Nothing', qui C # n'a pas.
StuartLC
32
class A {}
class B : A {}

public void SomeFunction()
{
    var someListOfB = new List<B>();
    someListOfB.Add(new B());
    someListOfB.Add(new B());
    someListOfB.Add(new B());
    SomeFunctionThatTakesA(someListOfB);
}

public void SomeFunctionThatTakesA(IEnumerable<A> input)
{
    // Before C# 4, you couldn't pass in List<B>:
    // cannot convert from
    // 'System.Collections.Generic.List<ConsoleApplication1.B>' to
    // 'System.Collections.Generic.IEnumerable<ConsoleApplication1.A>'
}

Fondamentalement, chaque fois que vous aviez une fonction qui prend un Enumerable d'un type, vous ne pouviez pas passer un Enumerable d'un type dérivé sans le cast explicitement.

Juste pour vous avertir d'un piège:

var ListOfB = new List<B>();
if(ListOfB is IEnumerable<A>)
{
    // In C# 4, this branch will
    // execute...
    Console.Write("It is A");
}
else if (ListOfB is IEnumerable<B>)
{
    // ...but in C# 3 and earlier,
    // this one will execute instead.
    Console.Write("It is B");
}

C'est du code horrible de toute façon, mais il existe et le changement de comportement en C # 4 peut introduire des bogues subtils et difficiles à trouver si vous utilisez une construction comme celle-ci.

Michael Stum
la source
Cela affecte donc les collections plus que tout, car dans c # 3, vous pouvez passer un type plus dérivé dans une méthode d'un type moins dérivé.
Razor
3
Oui, le grand changement est que IEnumerable prend désormais en charge cela, alors que ce n'était pas le cas auparavant.
Michael Stum
4

Depuis MSDN

L'exemple de code suivant montre la prise en charge de la covariance et de la contravariance pour les groupes de méthodes

static object GetObject() { return null; }
static void SetObject(object obj) { }

static string GetString() { return ""; }
static void SetString(string str) { }

static void Test()
{
    // Covariance. A delegate specifies a return type as object, 
    // but you can assign a method that returns a string.
    Func<object> del = GetString;

    // Contravariance. A delegate specifies a parameter type as string, 
    // but you can assign a method that takes an object.
    Action<string> del2 = SetObject;
}
Kamran Bigdely
la source
4

Contravariance

Dans le monde réel, vous pouvez toujours utiliser un abri pour animaux au lieu d'un abri pour lapins car chaque fois qu'un refuge pour animaux héberge un lapin, c'est un animal. Cependant, si vous utilisez un refuge pour lapins au lieu d'un refuge pour animaux, son personnel peut être mangé par un tigre.

Dans le code, cela signifie que si vous avez un , IShelter<Animal> animalsvous pouvez simplement écrire IShelter<Rabbit> rabbits = animals si vous promettez et l' utilisation Tdans la IShelter<T>seule en tant que paramètres de la méthode comme ceci:

public class Contravariance
{
    public class Animal { }
    public class Rabbit : Animal { }

    public interface IShelter<in T>
    {
        void Host(T thing);
    }

    public void NoCompileErrors()
    {
        IShelter<Animal> animals = null;
        IShelter<Rabbit> rabbits = null;

        rabbits = animals;
    }
}

et remplacer un élément par un élément plus générique, c'est-à-dire réduire la variance ou introduire une contre- variance.

Covariance

Dans le monde réel, vous pouvez toujours utiliser un fournisseur de lapins au lieu d'un fournisseur d'animaux car chaque fois qu'un fournisseur de lapins vous donne un lapin, c'est un animal. Cependant, si vous utilisez un fournisseur d'animaux au lieu d'un fournisseur de lapins, vous pouvez être mangé par un tigre.

Dans le code, cela signifie que si vous avez un, ISupply<Rabbit> rabbitsvous pouvez simplement écrire ISupply<Animal> animals = rabbits si vous promettez et utiliser Tdans la ISupply<T>méthode only as des valeurs de retour comme suit:

public class Covariance
{
    public class Animal { }
    public class Rabbit : Animal { }

    public interface ISupply<out T>
    {
        T Get();
    }

    public void NoCompileErrors()
    {
        ISupply<Animal> animals = null;
        ISupply<Rabbit> rabbits = null;

        animals = rabbits;
    }
}

et remplacer un élément par un élément plus dérivé, c'est-à-dire augmenter la variance ou introduire la co- variance.

Dans l'ensemble, il ne s'agit que d'une promesse vérifiable lors de la compilation de votre part selon laquelle vous traiteriez un type générique d'une certaine manière pour garder le type en sécurité et ne pas faire manger personne.

Vous voudrez peut-être lire ceci pour revenir en arrière.

Ivan Rybalko
la source
vous pouvez vous faire manger par un tigre qui valait un vote positif
javadba
Votre commentaire contravarianceest intéressant. J'y lis comme indiquant une exigence opérationnelle : que le type plus général doit prendre en charge les cas d'utilisation de tous les types qui en découlent. Donc, dans ce cas, le refuge pour animaux doit être en mesure de prendre en charge l'hébergement de tous les types d'animaux. Dans ce cas, l'ajout d'une nouvelle sous-classe pourrait casser la superclasse! Autrement dit, si nous ajoutons un sous-type de Tyrannosaurus Rex, cela pourrait détruire notre refuge pour animaux existant .
javadba
(A continué). Cela diffère fortement de la covariance qui est clairement décrite structurellement : tous les sous-types plus spécifiques supportent les opérations définies dans le super type - mais pas nécessairement de la même manière.
javadba
3

Le délégué convertisseur m'aide à visualiser les deux concepts fonctionnant ensemble:

delegate TOutput Converter<in TInput, out TOutput>(TInput input);

TOutputreprésente la covariance où une méthode renvoie un type plus spécifique .

TInputreprésente une contravariance où une méthode reçoit un type moins spécifique .

public class Dog { public string Name { get; set; } }
public class Poodle : Dog { public void DoBackflip(){ System.Console.WriteLine("2nd smartest breed - woof!"); } }

public static Poodle ConvertDogToPoodle(Dog dog)
{
    return new Poodle() { Name = dog.Name };
}

List<Dog> dogs = new List<Dog>() { new Dog { Name = "Truffles" }, new Dog { Name = "Fuzzball" } };
List<Poodle> poodles = dogs.ConvertAll(new Converter<Dog, Poodle>(ConvertDogToPoodle));
poodles[0].DoBackflip();
woggles
la source