Zrozumienie kowariantnych i kontrawariantnych interfejsów w C #

86

Natknąłem się na nie w podręczniku, który czytam na C #, ale mam trudności ze zrozumieniem ich, prawdopodobnie z powodu braku kontekstu.

Czy istnieje dobre, zwięzłe wyjaśnienie, czym one są i do czego są przydatne?

Edytuj dla wyjaśnienia:

Kowariantny interfejs:

interface IBibble<out T>
.
.

Interfejs kontrawariantny:

interface IBibble<in T>
.
.
NibblyPig
źródło
3
To jest krótkie i dobre wyjaśnienie IMHO: blogs.msdn.com/csharpfaq/archive/2010/02/16/ ...
digEmAll Kwietnia
Może się przydać: Post na blogu
Krunal,
Hmm, to jest dobre, ale nie wyjaśnia dlaczego, co mnie naprawdę zaskakuje.
NibblyPig

Odpowiedzi:

144

Za pomocą <out T>można traktować odwołanie do interfejsu jako jedno w górę w hierarchii.

Za pomocą <in T>można traktować odwołanie do interfejsu jako jeden w dół w hierarchii.

Spróbuję wyjaśnić to bardziej po angielsku.

Powiedzmy, że pobierasz listę zwierząt ze swojego zoo i zamierzasz je przetworzyć. Wszystkie zwierzęta (w twoim zoo) mają imię i unikalny identyfikator. Niektóre zwierzęta to ssaki, niektóre to gady, niektóre to płazy, niektóre to ryby itd., Ale wszystkie to zwierzęta.

Tak więc, korzystając z listy zwierząt (która zawiera zwierzęta różnych typów), możesz powiedzieć, że wszystkie zwierzęta mają imiona, więc oczywiście byłoby bezpiecznie uzyskać nazwy wszystkich zwierząt.

A co jeśli masz tylko listę ryb, ale musisz traktować je jak zwierzęta, czy to działa? Intuicyjnie powinno działać, ale w C # 3.0 i wcześniejszych ten fragment kodu nie będzie się kompilował:

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

Powodem tego jest to, że kompilator nie „wie”, co zamierzasz lub nie możesz zrobić z kolekcją zwierząt po jej pobraniu. Z tego, co wie, może istnieć sposób IEnumerable<T>na ponowne umieszczenie obiektu na liście, co potencjalnie pozwoliłoby na umieszczenie zwierzęcia, które nie jest rybą, w kolekcji, która powinna zawierać tylko ryby.

Innymi słowy, kompilator nie może zagwarantować, że jest to niedozwolone:

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

Więc kompilator po prostu odmawia skompilowania twojego kodu. To jest kowariancja.

Spójrzmy na kontrawariancję.

Ponieważ nasze zoo może obsłużyć wszystkie zwierzęta, z pewnością poradzi sobie z rybami, więc spróbujmy dodać trochę ryb do naszego zoo.

W C # 3.0 i wcześniej nie kompiluje się to:

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

W tym przypadku kompilator mógłby zezwolić na ten fragment kodu, mimo że metoda zwraca List<Animal>po prostu dlatego, że wszystkie ryby są zwierzętami, więc jeśli zmieniliśmy typy na takie:

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

Wtedy to zadziałałoby, ale kompilator nie może stwierdzić, że nie próbujesz tego zrobić:

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

Ponieważ lista jest w rzeczywistości listą zwierząt, jest to niedozwolone.

Zatem przeciwwariancja i współwariancja to sposób, w jaki traktujesz odniesienia do obiektów i co możesz z nimi robić.

inI outsłowa kluczowe w C # 4.0 konkretnie znaki interfejs do jednego lub drugiego. Dzięki in, możesz umieścić typ ogólny (zwykle T) w pozycjach wejściowych , co oznacza argumenty metody i właściwości tylko do zapisu.

W przypadku outmożna umieścić typ ogólny w pozycjach wyjściowych , które są wartościami zwracanymi metod, właściwościami tylko do odczytu i parametrami wyjściowymi metody.

Pozwoli ci to zrobić to, co zamierzasz zrobić z kodem:

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> ma zarówno kierunek do wewnątrz, jak i na zewnątrz na T, więc nie jest to ani kowariancja, ani przeciwwariancja, ale interfejs, który pozwala na dodawanie obiektów, takich jak ten:

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

pozwoli ci to zrobić:

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

Oto kilka filmów przedstawiających koncepcje:

Oto przykład:

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

Bez tych znaków można by skompilować:

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

albo to:

public List<Base> GetBases() ...
List<Descendant> descendants = GetBases(); <-- uh-oh, we try to treat all Bases
                                               as Descendants
Lasse V. Karlsen
źródło
Hmm, czy byłbyś w stanie wyjaśnić cel kowariancji i kontrawariancji? Pomogłoby mi to lepiej to zrozumieć.
NibblyPig
1
Zobacz ostatni fragment, przed którym kompilator zapobiegał wcześniej, celem in i out jest powiedzenie, co możesz zrobić z interfejsami (lub typami), które są bezpieczne, aby kompilator nie przeszkodził ci w robieniu bezpiecznych rzeczy .
Lasse V. Karlsen
Doskonała odpowiedź, obejrzałem filmy, które były bardzo pomocne, i w połączeniu z twoim przykładem mam to teraz posortowane. Pozostaje tylko jedno pytanie: dlaczego wymagane jest „wyjście” i „wejście”, dlaczego visual studio nie wie automatycznie, co próbujesz zrobić (lub jaka jest tego przyczyna)?
NibblyPig
Automagia „Widzę, co tam próbujesz zrobić” jest zwykle źle widziana, jeśli chodzi o deklarowanie takich rzeczy, jak klasy, lepiej jest, aby programista wyraźnie zaznaczył typy. Możesz spróbować dodać „in” do klasy, która ma metody zwracające T, a kompilator będzie narzekał. Wyobraź sobie, co by się stało, gdyby po cichu usunął „wejście”, które wcześniej automatycznie dla Ciebie dodał.
Lasse V. Karlsen
1
Jeśli jedno słowo kluczowe wymaga tak długiego wyjaśnienia, coś jest wyraźnie nie tak. Moim zdaniem C # stara się być zbyt sprytny w tym konkretnym przypadku. Niemniej jednak dziękuję za fajne wyjaśnienie.
rr-
7

Ten post jest najlepszym, jaki przeczytałem na ten temat

Krótko mówiąc, kowariancja / kontrawariancja / niezmienność zajmuje się automatycznym rzutowaniem typów (od podstawowego do wyprowadzonego i odwrotnie). Rzuty tego typu są możliwe tylko wtedy, gdy przestrzegane są pewne gwarancje dotyczące operacji odczytu / zapisu wykonywanych na rzutowanych obiektach. Przeczytaj post, aby uzyskać więcej informacji.

Ben G.
źródło
5
Link wygląda na martwy. Oto zarchiwizowana wersja: web.archive.org/web/20140626123445/http://adamnathan.co.uk/…
si618
1
Jeszcze bardziej podoba mi się to wyjaśnienie: codepureandsimple.com/…
volkit