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 string
na int
spowoduje 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 ?
c#
.net
c#-4.0
covariance
contravariance
cuongle
źródło
źródło
Odpowiedzi:
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:
źródło
int
nie jest podtypemobject
. Fakt, że wymagana jest zmiana reprezentacji, jest tylko tego konsekwencją.Int32
ma 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 podtypobject
, ale kompilator automatycznie radzi sobie z tym za każdym razem, gdy typ wartości jest przypisany do kompatybilnego interfejsu lub do czegoś typuobject
.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:
Możesz myśleć o tym,
strings
że ma następującą reprezentację: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:
Zasadniczo jest to ta sama reprezentacja, z wyjątkiem tego, że teraz odniesienia są odniesieniami do obiektów:
Reprezentacja jest taka sama. Odniesienia są po prostu traktowane inaczej; nie masz już dostępu do
string.Length
nieruchomości, ale nadal możesz zadzwonićobject.GetHashCode()
. Porównaj to ze zbiorem liczb int:Aby przekonwertować to
IEnumerable<object>
na dane, należy przekonwertować je, umieszczając w ramkach int:Ta konwersja wymaga czegoś więcej niż obsady.
źródło
this
odnosi 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.Myślę, że wszystko zaczyna się od określenia
LSP
(Zasada Zastępstwa Liskova), które klimaty:Ale typów wartości, na przykład,
int
nie można zastąpićobject
inC#
. Udowodnienie jest bardzo proste:Zwraca to
false
nawet, jeśli przypiszemy to samo „odniesienie” do obiektu.źródło
int
nie jest podtypem,object
więc zasada nie ma zastosowania. Twój „dowód” opiera się na reprezentacji pośredniejInteger
, która jest podtypemobject
i dla którego język ma niejawną konwersję (object obj1=myInt;
jest faktycznie rozszerzony doobject obj1=new Integer(myInt)
;).int
nie jest to podtypobject
. Ponadto LSP nie ma zastosowania, ponieważmyInt
,obj1
iobj2
odnoszą się do trzech różnych obiektów: jedenint
i dwa (ukryty)Integer
s.int
kluczowe C # jest aliasem dla BCLSystem.Int32
, który jest w rzeczywistości podtypemobject
(aliasSystem.Object
). W rzeczywistościint
klasaSystem.ValueType
bazowa jest klasą bazowąSystem.Object
. Spróbuj oceniając następujące wyrażenie i zobaczyć:typeof(int).BaseType.BaseType
. PrzyczynąReferenceEquals
zwracania fałszu jest tutaj to, żeint
jest 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.System.Int32
LubList<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.ValueType
bę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, ...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 ...
źródło