Почему ковариация и контравариантность не поддерживают тип ценности
IEnumerable<T>
является со-вариантом, но он не поддерживает тип значения, а только тип ссылки. Следующий простой код скомпилирован успешно:
IEnumerable<string> strList = new List<string>();
IEnumerable<object> objList = strList;
Но изменение с string
до int
приведет к ошибке компиляции:
IEnumerable<int> intList = new List<int>();
IEnumerable<object> objList = intList;
Причина объясняется в MSDN:
Отклонение применяется только к ссылочным типам; если вы укажете тип значения для параметра типа варианта, этот тип параметра является инвариантным для результирующего построенного типа.
Я искал и обнаружил, что в некоторых вопросах упоминается причина бокса между типом значения и ссылочным типом. Но это еще не все еще ясно, почему бокс является причиной?
Может ли кто-нибудь дать простое и подробное объяснение, почему ковариация и контравариантность не поддерживают тип ценности и как влияет на это бокс?
Ответы
Ответ 1
В принципе, дисперсия применяется, когда среда CLR может гарантировать, что ей не нужно делать какие-либо репрезентативные изменения значений. Все ссылки выглядят одинаково, поэтому вы можете использовать IEnumerable<string>
как IEnumerable<object>
без каких-либо изменений в представлении; сам собственный код не обязательно должен знать, что вы делаете со значениями, если инфраструктура гарантировала, что он определенно будет действительным.
Для типов значений это не работает - для обработки IEnumerable<int>
как IEnumerable<object>
, код с использованием последовательности должен знать, следует ли выполнять преобразование бокса или нет.
Возможно, вы захотите прочитать Eric Lippert сообщение в блоге о представлении и идентификации для получения дополнительной информации по этой теме в целом.
EDIT: перечитав сообщение блога Эрика, это, по крайней мере, столько же, что и личность, как представление, хотя эти два связаны. В частности:
Вот почему ковариантные и контравариантные преобразования типов интерфейса и делегата требуют, чтобы все переменные аргументы типа были ссылочными типами. Чтобы гарантировать, что вариантное преобразование ссылок всегда сохраняется с сохранением идентичности, все преобразования, связанные с аргументами типа, также должны сохраняться в идентичности. Самый простой способ гарантировать, что все нетривиальные преобразования в аргументах типа сохраняют сохранение идентичности, - это ограничить их ссылочными преобразованиями.
Ответ 2
Я думаю, что все начинается с определения LSP
(Принцип замещения Лискова), который дает:
если q (x) - свойство, доказуемое относительно объектов x типа T, то q (y) должно быть истинным для объектов y типа S, где S - подтип T.
Но типы значений, например int
, не могут быть заменены на object
в C#
.
Докажите очень просто:
int myInt = new int();
object obj1 = myInt ;
object obj2 = myInt ;
return ReferenceEquals(obj1, obj2);
Это возвращает false
, даже если мы назначаем ту же "ссылку" на объект.
Ответ 3
Возможно, проще понять, если вы думаете о базовом представлении (хотя это действительно является детальностью реализации). Вот набор строк:
IEnumerable<string> strings = new[] { "A", "B", "C" };
Вы можете думать о strings
как о следующем представлении:
[0] : string reference -> "A"
[1] : string reference -> "B"
[2] : string reference -> "C"
Это набор из трех элементов, каждый из которых является ссылкой на строку. Вы можете сделать это для коллекции объектов:
IEnumerable<object> objects = (IEnumerable<object>) strings;
В основном это одно и то же представление, кроме ссылок на ссылки на объекты:
[0] : object reference -> "A"
[1] : object reference -> "B"
[2] : object reference -> "C"
Представление такое же. Ссылки рассматриваются только по-разному; вы больше не можете получить доступ к свойству string.Length
, но вы все равно можете вызвать object.GetHashCode()
. Сравните это с коллекцией ints:
IEnumerable<int> ints = new[] { 1, 2, 3 };
[0] : int = 1
[1] : int = 2
[2] : int = 3
Чтобы преобразовать это значение в IEnumerable<object>
, данные должны быть преобразованы путем бокса по целям:
[0] : object reference -> 1
[1] : object reference -> 2
[2] : object reference -> 3
Это преобразование требует больше, чем приведение.
Ответ 4
Он доходит до детали реализации: типы значений реализованы по-разному для ссылочных типов.
Если вы заставляете типы значений обрабатываться как ссылочные типы (т.е. вставляйте их, например, ссылаясь на них через интерфейс), вы можете получить дисперсию.
Самый простой способ увидеть разницу - просто рассмотреть Array
: массив типов значений объединяется в память смежно (напрямую), где, поскольку массив типов Reference имеет только ссылку (указатель) смежно в Память; объекты, на которые направлены, выделены отдельно.
Другая (связанная) проблема (*) заключается в том, что (почти) все типы ссылок имеют одинаковое представление для целей дисперсии, и много кода не нужно знать о различии между типами, поэтому возможна совпадение и противоречие (и легко реализуется - часто просто путем исключения дополнительной проверки типов).
(*) Возможно, это одна и та же проблема...