web-dev-qa-db-fra.com

Covariance et contravariance, exemple du monde réel

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

Jusqu'à présent, les seuls exemples que j'ai vus sont les mêmes exemples de tableaux.

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

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

143
Vince Panuccio

Disons que vous avez une personne de classe et une classe qui en dérive, Maître. Vous avez des opérations qui prennent un IEnumerable<Person> comme argument. Dans votre classe d'école, vous avez une méthode qui retourne un IEnumerable<Teacher>. Covariance vous permet d’utiliser directement ce résultat pour les méthodes qui utilisent un IEnumerable<Person>, en substituant un type plus dérivé à un type moins dérivé (plus générique). Contravariance, de manière contre-intuitive, 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<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());
99
tvanfosson
// 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();

Pour être complet…

// 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??
124
Marcelo Cantos

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
108
CSharper

Les mots-clés in et out contrôlent les règles de transtypage 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
54
Jack

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

Étant donné la hiérarchie simple des classes:

enter image description here

Et en code:

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

Invariance (paramètres de type générique * pas * décorée avec in ou out mots-clés)

Apparemment, une telle méthode

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 qui est le cas)

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

Cependant, passer une collection de 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 une covariante - IList<T> est invariant, donc IList<LifeForm> accepte uniquement les collections (qui implémentent IList) où le type paramétré T doit être LifeForm.

Si la méthode implémentée de PrintLifeForms était malveillante (mais a la même signature de méthode), le compilateur empêche le passage de List<Giraffe> devient évident:

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

Puisque IList permet d’ajouter ou de supprimer des éléments, toute sous-classe de LifeForm pourrait ainsi être ajoutée au paramètre lifeForms et violerait le type de toute collection de types dérivés passée à la méthode. (Ici, la méthode malveillante essaierait 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é de out)

La covariance est largement utilisée avec des 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 consiste à faire en sorte qu'un type de collection générique covariant soit utilisé, par ex. IEnumerable (défini comme IEnumerable<out T>). IEnumerable ne dispose d'aucune méthode pour modifier la collection et, en conséquence de la covariance out, toute collection ayant un sous-type de LifeForm peut maintenant être transmise à la méthode:

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

PrintLifeForms peut maintenant être appelé avec Zebras, Giraffes et n’importe quel IEnumerable<> de toute sous-classe de LifeForm

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

La contradiction est fréquemment utilisée lorsque des fonctions sont transmises en tant que paramètres.

Voici un exemple de fonction qui prend un Action<Zebra> en tant que paramètre et l’appelle sur une instance connue d’un zèbre:

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 Action est défini comme Action<in T>, c’est-à-dire contravariant, ce qui signifie que pour Action<Zebra> myAction, que myAction peut être au "plus" a Action<Zebra>, mais les superclasses moins dérivées de Zebra sont également acceptables.

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

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

33
StuartLC

De MSDN

L'exemple de code suivant montre la prise en charge des covariances et des contravariances 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;
}
4
Kamran Bigdely

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

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

TOutput représente covariance lorsqu'une méthode retourne un type plus spécifique .

TInput représente contravariance où une méthode est passée à 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();
2
woggles