Przykład kowariancji i kontrawariancji w świecie rzeczywistym

162

Mam trochę problemów ze zrozumieniem, jak użyłbym kowariancji i kontrawariancji w prawdziwym świecie.

Jak dotąd jedyne przykłady, które widziałem, to ten sam przykład starej tablicy.

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

Byłoby miło zobaczyć przykład, który pozwoliłby mi go użyć podczas rozwoju, gdybym mógł zobaczyć, jak jest używany gdzie indziej.

Brzytwa
źródło
1
Badam kowariancję w odpowiedzi na (moje własne) pytanie: typy kowariancji: na przykładzie . Myślę, że uznasz to za interesujące i miejmy nadzieję, że pouczające.
Cristian Diaconescu

Odpowiedzi:

109

Powiedzmy, że masz klasę Person i klasę, która z niej pochodzi, Nauczycielu. Masz kilka operacji, które przyjmują IEnumerable<Person>argument jako argument. W swojej klasie School masz metodę, która zwraca IEnumerable<Teacher>. Kowariancja umożliwia bezpośrednie użycie tego wyniku dla metod, które przyjmują IEnumerable<Person>typ, zastępując typ bardziej pochodny typem mniej pochodnym (bardziej ogólnym). Kontrawariancja, wbrew intuicji, pozwala na użycie bardziej ogólnego typu, w którym określono bardziej pochodny typ.

Zobacz także Kowariancja i kontrawariancja w produktach generycznych w witrynie MSDN .

Zajęcia :

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);
    }
}

Użycie :

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
źródło
14
@FilipBartuzi - jeśli tak jak ja, kiedy pisałem tę odpowiedź, byłeś zatrudniony na uniwersytecie, który jest przykładem z prawdziwego świata.
tvanfosson,
5
Jak można to oznaczyć jako odpowiedź, skoro nie odpowiada na pytanie i nie podaje żadnego przykładu użycia co / contra variance w języku c #?
barakcaf
@barakcaf dodał przykład kontrawariancji. Nie jestem pewien, dlaczego nie widzisz przykładu kowariancji - być może musiałeś przewinąć kod w dół - ale dodałem kilka komentarzy na ten temat.
tvanfosson
@tvanfosson kod używa co / contra, myślę, że nie pokazuje, jak to zadeklarować. Przykład nie pokazuje użycia wejścia / wyjścia w deklaracji ogólnej, podczas gdy druga odpowiedź tak.
barakcaf
Tak więc, jeśli dobrze rozumiem, kowariancja jest tym, co pozwala na zasadę podstawiania Liskova w C #, czy to prawda?
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();

Dla pełności…

// 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
źródło
138
Podoba mi się ten realistyczny przykład. W zeszłym tygodniu właśnie pisałem kod pożerania osłów i bardzo się ucieszyłem, że mamy teraz kowariancję. :-)
Eric Lippert
4
Ten komentarz powyżej, w którym @javadba mówi THE EricLippert, co to jest kowariancja i kontrawariancja, jest realistycznym kowariantnym przykładem tego, jak mówię babci, jak ssać jajka! : p
iAteABug_And_iLiked_it
1
Pytanie nie dotyczyło tego, co mogą zrobić kontrawariancja i kowariancja , ale zadawało pytanie, dlaczego miałbyś ich używać . Twój przykład jest daleki od praktycznego, ponieważ też nie wymaga. Mogę stworzyć QuadrupedGobblera i traktować go jak siebie samego (przypisać go do IGobbler <Quadruped>) i nadal mogę pożerać Osły (mogę przekazać Osła do metody Gobble, która wymaga Quadruped). Nie jest wymagana żadna sprzeczność. To świetnie, że można traktować QuadrupedGobbler jako DonkeyGobbler, ale dlaczego musimy w tym przypadku, jeśli QuadrupedGobbler może już pochłaniają Osły?
wired_in
1
@wired_in Ponieważ kiedy zależy ci tylko na osłach, bardziej ogólne podejście może przeszkodzić. Na przykład, jeśli masz farmę, która dostarcza osły do ​​pożarcia, możesz to wyrazić jako void feed(IGobbler<Donkey> dg). Jeśli zamiast tego wziąłbyś IGobbler <Quadruped> jako parametr, nie mógłbyś przekazać smoka, który zjada tylko osły.
Marcelo Cantos
1
Spóźnij się na imprezę, ale to chyba najlepiej napisany przykład, jaki widziałem w SO. Ma sens, będąc śmiesznym. Będę musiał podnieść swoją grę z odpowiedziami ...
Jesse Williams
121

Oto, co zebrałem, aby pomóc mi zrozumieć różnicę

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
źródło
10
To najlepsza rzecz, jaką do tej pory widziałem, która jest jasna i zwięzła. Świetny przykład!
Rob L
6
Jak można zrzucić owoc do jabłka (w Contravarianceprzykładzie), kiedy Fruitjest rodzicem Apple?
Tobias Marschall
@TobiasMarschall, co oznacza, że ​​musisz nauczyć się więcej na temat „polimorfizmu”
snr
56

Słowa kluczowe in i out sterują regułami rzutowania kompilatora dla interfejsów i delegatów z parametrami ogólnymi:

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
Jacek
źródło
Zakładając, że ryba jest podtypem zwierzęcia. Swoją drogą świetna odpowiedź.
Rajan Prasad
48

Oto prosty przykład wykorzystujący hierarchię dziedziczenia.

Biorąc pod uwagę prostą hierarchię klas:

wprowadź opis obrazu tutaj

A w kodzie:

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

Niezmienność (tj. Parametry typu ogólnego * nie * ozdobione słowami kluczowymi inlub out)

Pozornie metoda taka jak ta

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

... powinien akceptować zbiór heterogeniczny: (co robi)

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

Jednak przekazanie kolekcji bardziej pochodnego typu kończy się niepowodzeniem!

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>'

Czemu? Ponieważ parametr ogólny IList<LifeForm>nie jest kowariantny - IList<T>jest niezmienny, więc IList<LifeForm>akceptuje tylko kolekcje (które implementują IList), w których Tmusi znajdować się sparametryzowany typ LifeForm.

Jeśli implementacja metody PrintLifeFormsbyła złośliwa (ale ma tę samą sygnaturę metody), powód, dla którego kompilator uniemożliwia przekazanie, List<Giraffe>staje się oczywisty:

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

Ponieważ IListzezwala na dodawanie lub usuwanie elementów, każda podklasa klasy LifeFormmogłaby zostać dodana do parametru lifeFormsi naruszyłaby typ dowolnej kolekcji typów pochodnych przekazanych do metody. (W tym przypadku złośliwa metoda spróbuje dodać Zebrado var myGiraffes). Na szczęście kompilator chroni nas przed tym niebezpieczeństwem.

Kowariancja (ogólna z typem sparametryzowanym ozdobiona out)

Kowariancja jest szeroko stosowana w niezmiennych kolekcjach (tj. Gdy nie można dodawać ani usuwać nowych elementów z kolekcji)

Rozwiązaniem powyższego przykładu jest zapewnienie, że używany jest kowariantny ogólny typ kolekcji, np. IEnumerable(Zdefiniowany jako IEnumerable<out T>). IEnumerablenie ma metod zmiany kolekcji, a w wyniku outkowariancji dowolny zbiór z podtypem LifeFormmoże być teraz przekazany do metody:

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

PrintLifeFormsmożna teraz nazywa się Zebras, Giraffesa każdy IEnumerable<>z dowolnej podklasyLifeForm

Kontrawariancja (ogólna z parametryzowanym typem ozdobiona in)

Kontrawariancja jest często używana, gdy funkcje są przekazywane jako parametry.

Oto przykład funkcji, która przyjmuje Action<Zebra>parametr jako parametr i wywołuje go na znanym wystąpieniu Zebry:

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

Zgodnie z oczekiwaniami działa to dobrze:

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

Intuicyjnie to się nie powiedzie:

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

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

Jednak to się udaje

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

i nawet to się udaje:

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

Czemu? Ponieważ Actionjest zdefiniowane jako Action<in T>, tj. Jest contravariant, co oznacza, że ​​dla Action<Zebra> myAction, które myActionmoże wynosić „co najwyżej” a Action<Zebra>, ale mniej pochodne nadklasy Zebrasą również dopuszczalne.

Chociaż na początku może to być nieintuicyjne (np. Jak można Action<object>przekazać parametr jako wymagający Action<Zebra>?), Jeśli rozpakujesz kroki, zauważysz, że sama wywoływana funkcja ( PerformZebraAction) jest odpowiedzialna za przekazywanie danych (w tym przypadku Zebrainstancja ) do funkcji - dane nie pochodzą z kodu wywołującego.

Ze względu na odwrotne podejście polegające na używaniu funkcji wyższego rzędu w ten sposób, w momencie Actionwywołania funkcji jest ona bardziej pochodną Zebrainstancją, która jest wywoływana względem zebraActionfunkcji (przekazywana jako parametr), chociaż sama funkcja używa mniej pochodnego typu.

StuartLC
źródło
7
Jest to świetne wyjaśnienie różnych opcji wariancji, ponieważ omawia przykład, a także wyjaśnia, dlaczego kompilator ogranicza lub zezwala bez słów kluczowych
wejścia
Gdzie jest insłowo kluczowe używane do określenia kontrawariancji ?
javadba
@javadba w powyższym Action<in T>i Func<in T, out TResult>są kontrawariantne w typie danych wejściowych. (Moje przykłady wykorzystują istniejące typy niezmiennicze (List), kowariantne (IEnumerable) i kontrawariantne (Action, Func))
StuartLC
Ok, nie robię C#tego, nie wiedziałbym tego.
javadba
Jest dość podobny w Scali, tylko inna składnia - [+ T] byłaby kowariantna w T, [-T] byłaby kontrawariantna w T, Scala może również wymusić ograniczenie „pomiędzy” i podklasę rozwiązłą „Nic”, która C # nie ma.
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>'
}

Zasadniczo za każdym razem, gdy masz funkcję, która przyjmuje Enumerable jednego typu, nie możesz przekazać Enumerable typu pochodnego bez jawnego rzutowania.

Jednak tylko po to, aby ostrzec Cię przed pułapką:

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");
}

To i tak okropny kod, ale istnieje, a zmieniające się zachowanie w C # 4 może wprowadzić subtelne i trudne do znalezienia błędy, jeśli użyjesz takiej konstrukcji.

Michael Stum
źródło
Więc ma to wpływ na kolekcje bardziej niż cokolwiek innego, ponieważ w języku C # 3 można przekazać bardziej pochodny typ do metody mniej pochodnego typu.
Razor
3
Tak, duża zmiana polega na tym, że IEnumerable teraz to obsługuje, podczas gdy wcześniej nie było.
Michael Stum
4

Z MSDN

Poniższy przykład kodu przedstawia obsługę kowariancji i kontrawariancji dla grup metod

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
źródło
4

Sprzeczność

W prawdziwym świecie zawsze możesz skorzystać ze schroniska dla zwierząt zamiast schroniska dla królików, ponieważ za każdym razem, gdy schronisko jest gospodarzem królika, jest to zwierzę. Jeśli jednak zamiast schroniska dla zwierząt użyjesz schroniska dla królików, jego personel może zostać zjedzony przez tygrysa.

W kodzie, oznacza to, że jeśli masz IShelter<Animal> animalsmożna po prostu napisać IShelter<Rabbit> rabbits = animals , jeśli obiecasz i zastosowanie Tw IShelter<T>jedynie jako parametry metody tak:

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;
    }
}

i zamień pozycję na bardziej ogólną, tj. zmniejsz wariancję lub wprowadź kontrawariancję.

Kowariancja

W prawdziwym świecie zawsze możesz skorzystać z dostawcy królików zamiast dostawcy zwierząt, ponieważ za każdym razem, gdy dostawca królika daje ci królika, jest to zwierzę. Jeśli jednak korzystasz z dostawcy zwierząt zamiast dostawcy królika, możesz zostać zjedzony przez tygrysa.

W kodzie, oznacza to, że jeśli masz ISupply<Rabbit> rabbitsmożna po prostu napisać ISupply<Animal> animals = rabbits , jeśli obiecasz i zastosowanie Tw ISupply<T>jedynie jako wartości zwracane metoda tak:

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;
    }
}

i zamień element na bardziej pochodny, tj. zwiększ wariancję lub wprowadź ko- wariancję.

Podsumowując, jest to po prostu obietnica, którą możesz sprawdzić w czasie kompilacji , że będziesz traktować typ ogólny w określony sposób, aby zachować bezpieczeństwo typu i nie dać nikogo zjeść.

Możesz to przeczytać, aby podwójnie owinąć głowę wokół tego.

Ivan Rybalko
źródło
możesz zostać pożarty przez tygrysa To było warte uznania
javadba
Twój komentarz contravariancejest interesujący. Czytam to jako wskazanie wymogu operacyjnego : bardziej ogólny typ musi obsługiwać przypadki użycia wszystkich typów wywodzących się z niego. Tak więc w tym przypadku schronisko dla zwierząt musi być w stanie zapewnić schronienie dla każdego rodzaju zwierząt. W takim przypadku dodanie nowej podklasy może spowodować przerwanie superklasy! To znaczy - jeśli dodamy podtyp Tyrannosaurus Rex , może to zrujnować nasze istniejące schronisko dla zwierząt .
javadba
(Nieprzerwany). Różni się to znacznie od kowariancji, która jest wyraźnie opisana strukturalnie : wszystkie bardziej szczegółowe podtypy obsługują operacje zdefiniowane w typie nadrzędnym - ale niekoniecznie w ten sam sposób.
javadba
3

Delegat konwertera pomaga mi wizualizować obie koncepcje współpracujące ze sobą:

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

TOutputreprezentuje kowariancję, w której metoda zwraca bardziej szczegółowy typ .

TInputreprezentuje kontrawariancję, w której metoda jest przekazywana do mniej określonego typu .

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
źródło