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();
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();
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();
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ć.
in
I out
sł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 out
moż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();
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();
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)
{
IBibbleOut<Base> b = GetOutDescendant();
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