Dlaczego kowariancja i kontrawariancja nie obsługują typu wartości

149

IEnumerable<T>jest kowariancją, ale nie obsługuje typu wartości, tylko typ referencyjny. Poniższy prosty kod został pomyślnie skompilowany:

IEnumerable<string> strList = new List<string>();
IEnumerable<object> objList = strList;

Ale zmiana z stringna intspowoduje błąd kompilacji:

IEnumerable<int> intList = new List<int>();
IEnumerable<object> objList = intList;

Powód jest wyjaśniony w MSDN :

Wariancja dotyczy tylko typów referencyjnych; jeśli określisz typ wartości dla parametru typu wariantu, ten parametr typu jest niezmienny dla wynikowego typu skonstruowanego.

Przeszukałem i stwierdziłem, że w niektórych pytaniach wspomniano, że powodem jest boksowanie między typem wartości a typem odniesienia . Ale to wciąż nie wyjaśnia mi zbytnio, dlaczego powodem jest boks?

Czy ktoś mógłby podać proste i szczegółowe wyjaśnienie, dlaczego kowariancja i kontrawariancja nie obsługują typu wartości i jak wpływa na to boks ?

cuongle
źródło
3
zobacz także odpowiedź Erica na moje podobne pytanie: stackoverflow.com/questions/4096299/…
thorn̈

Odpowiedzi:

126

Zasadniczo wariancja ma zastosowanie, gdy środowisko CLR może zapewnić, że nie musi dokonywać żadnych reprezentacyjnych zmian wartości. Wszystkie odwołania wyglądają tak samo - możesz więc użyć IEnumerable<string>jakoIEnumerable<object> bez żadnych zmian w reprezentacji; sam kod natywny nie musi w ogóle wiedzieć, co robisz z wartościami, o ile infrastruktura gwarantuje, że na pewno będzie poprawna.

W przypadku typów wartości to nie działa - aby traktować IEnumerable<int>jako plikIEnumerable<object> , kod używający sekwencji musiałby wiedzieć, czy wykonać konwersję pudełkową, czy nie.

Możesz przeczytać wpis na blogu Erica Lipperta na temat reprezentacji i tożsamości aby uzyskać więcej ogólnych informacji na ten temat.

EDYCJA: Po ponownym przeczytaniu posta na blogu Erica, chodzi przynajmniej w takim samym stopniu o tożsamość, jak o reprezentację, chociaż te dwa elementy są połączone. W szczególności:

Dlatego konwersje kowariantne i kontrawariantne typów interfejsu i delegatów wymagają, aby wszystkie argumenty różnych typów były typami odwołań. Aby upewnić się, że konwersja odwołania do wariantu zawsze zachowuje tożsamość, wszystkie konwersje obejmujące argumenty typu muszą również zachowywać tożsamość. Najłatwiejszym sposobem na zapewnienie, że wszystkie nietrywialne konwersje argumentów typu zachowują tożsamość, jest ograniczenie ich do konwersji odwołań.

Jon Skeet
źródło
5
@CuongLe: Cóż, w pewnym sensie jest to szczegół implementacji, ale uważam, że jest to podstawowy powód ograniczenia.
Jon Skeet,
2
@ AndréCaron: Wpis na blogu Erica jest tutaj ważny - to nie tylko reprezentacja, ale także ochrona tożsamości. Ale zachowanie reprezentacji oznacza, że ​​wygenerowany kod wcale nie musi się tym przejmować.
Jon Skeet
1
Dokładniej, tożsamość nie może zostać zachowana, ponieważ intnie jest podtypem object. Fakt, że wymagana jest zmiana reprezentacji, jest tylko tego konsekwencją.
André Caron
3
Dlaczego int nie jest podtypem obiektu? Int32 dziedziczy po System.ValueType, który dziedziczy po System.Object.
David Klempfner
1
@DavidKlempfner Myślę, że komentarz @ AndréCaron jest źle sformułowany. Dowolny typ wartości, na przykład Int32ma dwie formy reprezentacji: „w pudełku” i „bez opakowania”. Kompilator musi wstawić kod, aby przekonwertować z jednej postaci na drugą, nawet jeśli jest to zwykle niewidoczne na poziomie kodu źródłowego. W efekcie tylko forma „pudełkowa” jest uważana przez bazowy system za podtyp object, ale kompilator automatycznie radzi sobie z tym za każdym razem, gdy typ wartości jest przypisany do kompatybilnego interfejsu lub do czegoś typu object.
Steve
10

Być może łatwiej będzie to zrozumieć, jeśli pomyślisz o podstawowej reprezentacji (nawet jeśli tak naprawdę jest to szczegół implementacji). Oto zbiór ciągów:

IEnumerable<string> strings = new[] { "A", "B", "C" };

Możesz myśleć o tym, stringsże ma następującą reprezentację:

[0]: odwołanie do ciągu -> „A”
[1]: odwołanie do ciągu -> "B"
[2]: odwołanie do ciągu -> "C"

Jest to zbiór trzech elementów, z których każdy jest odniesieniem do łańcucha. Możesz przesłać to do kolekcji obiektów:

IEnumerable<object> objects = (IEnumerable<object>) strings;

Zasadniczo jest to ta sama reprezentacja, z wyjątkiem tego, że teraz odniesienia są odniesieniami do obiektów:

[0]: odwołanie do obiektu -> „A”
[1]: odniesienie do obiektu -> „B”
[2]: odniesienie do obiektu -> "C"

Reprezentacja jest taka sama. Odniesienia są po prostu traktowane inaczej; nie masz już dostępu do string.Lengthnieruchomości, ale nadal możesz zadzwonić object.GetHashCode(). Porównaj to ze zbiorem liczb int:

IEnumerable<int> ints = new[] { 1, 2, 3 };
[0]: int = 1
[1]: int = 2
[2]: int = 3

Aby przekonwertować to IEnumerable<object>na dane, należy przekonwertować je, umieszczając w ramkach int:

[0]: odniesienie do obiektu -> 1
[1]: odniesienie do obiektu -> 2
[2]: odniesienie do obiektu -> 3

Ta konwersja wymaga czegoś więcej niż obsady.

Martin Liversage
źródło
2
Boks to nie tylko „szczegół implementacji”. Typy wartości w pudełkach są przechowywane w taki sam sposób, jak obiekty klas i zachowują się, o ile świat zewnętrzny może powiedzieć, jak obiekty klas. Jedyną różnicą jest to, że w ramach definicji opakowanego typu wartości thisodnosi się do struktury, której pola nakładają się na pola obiektu sterty, który ją przechowuje, zamiast odwoływać się do obiektu, który je przechowuje. Nie ma prostego sposobu, aby instancja typu wartości opakowanej mogła pobrać odwołanie do otaczającego obiektu sterty.
supercat
7

Myślę, że wszystko zaczyna się od określenia LSP(Zasada Zastępstwa Liskova), które klimaty:

jeśli q (x) jest właściwością możliwą do udowodnienia w odniesieniu do obiektów x typu T, to q (y) powinno być prawdziwe dla obiektów y typu S, gdzie S jest podtypem T.

Ale typów wartości, na przykład, intnie można zastąpić objectin C#. Udowodnienie jest bardzo proste:

int myInt = new int();
object obj1 = myInt ;
object obj2 = myInt ;
return ReferenceEquals(obj1, obj2);

Zwraca to falsenawet, jeśli przypiszemy to samo „odniesienie” do obiektu.

Tigran
źródło
1
Myślę, że stosujesz właściwą zasadę, ale nie ma dowodów, które można by wykonać: intnie jest podtypem, objectwięc zasada nie ma zastosowania. Twój „dowód” opiera się na reprezentacji pośredniej Integer, która jest podtypem objecti dla którego język ma niejawną konwersję ( object obj1=myInt;jest faktycznie rozszerzony do object obj1=new Integer(myInt);).
André Caron
Język dba o poprawne rzutowanie między typami, ale zachowanie ints nie odpowiada temu, którego oczekiwalibyśmy od podtypu obiektu.
Tigran
Chodzi mi o to, że intnie jest to podtyp object. Ponadto LSP nie ma zastosowania, ponieważ myInt, obj1i obj2odnoszą się do trzech różnych obiektów: jeden inti dwa (ukryty) Integers.
André Caron
22
@ André: C # to nie Java. Słowo intkluczowe C # jest aliasem dla BCL System.Int32, który jest w rzeczywistości podtypem object(alias System.Object). W rzeczywistości intklasa System.ValueTypebazowa jest klasą bazową System.Object. Spróbuj oceniając następujące wyrażenie i zobaczyć: typeof(int).BaseType.BaseType. Przyczyną ReferenceEqualszwracania fałszu jest tutaj to, że intjest umieszczony w dwóch oddzielnych polach, a tożsamość każdego pudełka jest inna dla każdego innego pudełka. Zatem dwie operacje pakowania zawsze dają dwa obiekty, które nigdy nie są identyczne, niezależnie od wartości w pudełku.
Allon Guralnek
@AllonGuralnek: Każdy typ wartości (np. System.Int32Lub List<String>.Enumerator) w rzeczywistości reprezentuje dwa rodzaje rzeczy: typ miejsca przechowywania i typ obiektu sterty (czasami nazywany „opakowanym typem wartości”). Lokalizacje magazynowe, z których wywodzą się typy, System.ValueTypebędą zawierać te pierwsze; obiekty sterty, których typy są w podobny sposób, będą zawierać te drugie. W większości języków istnieje poszerzająca się obsada od pierwszego do drugiego, a zwężająca się od drugiego do pierwszego. Zwróć uwagę, że podczas gdy typy wartości w pudełkach mają ten sam deskryptor typu, co lokalizacje przechowywania typu wartości, ...
supercat
3

Sprowadza się to do szczegółu implementacji: typy wartości są implementowane inaczej niż typy referencyjne.

Jeśli wymuszasz, aby typy wartości były traktowane jako typy referencyjne (tj. Umieszczaj je w pudełku, np. Odwołując się do nich przez interfejs), możesz uzyskać wariancję.

Najłatwiejszym sposobem dostrzeżenia różnicy jest po prostu rozważenie Array: tablica typów wartości jest umieszczana w pamięci w sposób ciągły (bezpośrednio), podczas gdy jako tablica typów referencyjnych tylko odwołanie (wskaźnik) jest ciągłe w pamięci; wskazywane obiekty są przydzielane oddzielnie.

Inną (pokrewną) kwestią (*) jest to, że (prawie) wszystkie typy referencyjne mają tę samą reprezentację dla celów wariancji, a znaczna część kodu nie musi znać różnicy między typami, więc współ- i przeciwwariancja jest możliwa (i łatwo zaimplementowane - często po prostu przez pominięcie dodatkowego sprawdzania typu).

(*) Może to być ten sam problem ...

Mark Hurd
źródło