Почему ковариация и контравариантность не поддерживают тип значения


149

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 :

Дисперсия применяется только к ссылочным типам; если вы указываете тип значения для параметра типа варианта, этот параметр типа является инвариантным для результирующего составного типа.

Я искал и обнаружил, что в некоторых вопросах упоминается причина - бокс между типом значения и ссылочным типом . Но это все еще не проясняет мой разум, почему бокс является причиной?

Может ли кто-нибудь дать простое и подробное объяснение, почему ковариация и контравариантность не поддерживают тип значения и как бокс влияет на это?


3
см. также ответ Эрика на мой похожий вопрос: stackoverflow.com/questions/4096299/…
thorn̈

1
возможный дубликат объекта cant-convert-value-type-array-to-params-object
nawfal

Ответы:


126

В основном, дисперсия применяется, когда CLR может гарантировать, что ему не нужно вносить какие-либо репрезентативные изменения в значения. Ссылки выглядят одинаково - так что вы можете использовать IEnumerable<string>как IEnumerable<object>без каких-либо изменений в представлении; самому нативному коду вообще не нужно знать, что вы делаете со значениями, если инфраструктура гарантирует, что он определенно будет действительным.

Для типов значений, которые не работают - лечить IEnumerable<int>как IEnumerable<object>код с использованием последовательности должен знать , следует ли выполнять преобразование бокса или нет.

Возможно, вы захотите прочитать сообщение Эрика Липперта в блоге о представительстве и идентичности, чтобы узнать больше об этой теме в целом.

РЕДАКТИРОВАТЬ: Перечитав сообщение в блоге Эрика самостоятельно, это по крайней мере так же, как личность, как представление, хотя оба связаны. В частности:

Вот почему ковариантные и контравариантные преобразования типов интерфейса и делегата требуют, чтобы все переменные аргументы типа имели ссылочные типы. Чтобы преобразование ссылки на вариант всегда сохраняло идентичность, все преобразования, включающие аргументы типа, также должны сохранять идентичность. Самый простой способ убедиться, что все нетривиальные преобразования в аргументах типа сохраняют идентичность, - это ограничить их ссылочными преобразованиями.


5
@CuongLe: Ну, в некоторых смыслах это деталь реализации, но я считаю, что это является основной причиной ограничения.
Джон Скит

2
@ AndréCaron: пост Эрика здесь важен - это не только представление, но и сохранение личности. Но сохранение представления означает, что сгенерированный код не должен заботиться об этом вообще.
Джон Скит

1
Точно, идентичность не может быть сохранена, потому что intне является подтипом object. Тот факт, что требуется изменение представительства, является лишь следствием этого.
Андре Карон

3
Как int не подтип объекта? Int32 наследуется от System.ValueType, который наследуется от System.Object.
Дэвид Клемпфнер

1
@DavidKlempfner Я думаю, что @ AndréCaron комментарий плохо сформулирован. Любой тип значения, например, Int32имеет две репрезентативные формы: «в штучной упаковке» и «без штучной упаковки». Компилятор должен вставить код для преобразования из одной формы в другую, хотя обычно он невидим на уровне исходного кода. В сущности, подтипом системы рассматривается только «коробочная» форма object, но компилятор автоматически справляется с этим всякий раз, когда тип значения назначается совместимому интерфейсу или чему-либо типа object.
Стив

10

Возможно, это будет легче понять, если вы подумаете о базовом представлении (даже если это действительно детали реализации). Вот коллекция строк:

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

Вы можете думать о том, чтобы stringsиметь следующее представление:

[0]: ссылка на строку -> "A"
[1]: ссылка на строку -> "B"
[2]: ссылка на строку -> "C"

Это коллекция из трех элементов, каждый из которых является ссылкой на строку. Вы можете привести это к коллекции объектов:

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

По сути, это то же представление, за исключением того, что теперь ссылки являются ссылками на объекты:

[0]: ссылка на объект -> "A"
[1]: ссылка на объект -> "B"
[2]: ссылка на объект -> "C"

Представление то же самое. Ссылки просто по-разному трактуются; Вы больше не можете получить доступ к string.Lengthсобственности, но вы все равно можете позвонить object.GetHashCode(). Сравните это с коллекцией целых:

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

Чтобы преобразовать это IEnumerable<object>в данные, необходимо преобразовать их в боксы:

[0]: ссылка на объект -> 1
[1]: ссылка на объект -> 2
[2]: ссылка на объект -> 3

Это преобразование требует больше, чем приведение.


2
Бокс - это не просто «деталь реализации». Типы в штучной упаковке хранятся так же, как и объекты классов, и ведут себя, насколько может сказать внешний мир, как объекты классов. Единственное отличие состоит в том, что в определении типа значения в штучной упаковке thisподразумевается структура, чьи поля перекрывают поля объекта кучи, в котором она хранится, а не ссылаются на объект, который их содержит. Не существует чистого способа для экземпляра типа в штучной упаковке получить ссылку на включающий объект кучи.
суперкат

7

Я думаю, что все начинается с определения 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даже если мы назначаем ту же «ссылку» на объект.


1
Я думаю, что вы используете правильный принцип, но нет никаких доказательств: intэто не подтип, objectпоэтому этот принцип не применяется. Ваше «доказательство» опирается на промежуточное представление Integer, которое является подтипом objectи для которого язык имеет неявное преобразование ( object obj1=myInt;фактически расширяется до object obj1=new Integer(myInt);).
Андре Карон

Язык заботится о правильном приведении типов между типами, но поведение int не соответствует тому, которое мы ожидаем от подтипа объекта.
Тигран

Весь мой смысл именно в том, что intэто не подтип object. Более того, LSP не применяется потому myInt, что obj1и obj2ссылаются на три разных объекта: один intи два (скрытые) Integer.
Андре Карон

22
@ Андре: C # это не Java. intКлючевое слово C # является псевдонимом для BCL System.Int32, который на самом деле является подтипом object(псевдоним System.Object). Фактически, intбазовый класс - System.ValueTypeэто базовый класс System.Object. Попробуйте оценить следующее выражение и см typeof(int).BaseType.BaseType. Причина, по которой ReferenceEqualsвозвращается значение false, заключается в том, что intблок упакован в два отдельных блока, а идентификационные данные каждого блока различны для любого другого блока. Таким образом, операция с двумя боксами всегда дает два объекта, которые никогда не бывают идентичными, независимо от значения в штучной упаковке.
Аллон Гуралнек

@AllonGuralnek: Каждый тип значения (например, System.Int32или List<String>.Enumerator) на самом деле представляет два типа вещей: тип места хранения и тип объекта кучи (иногда называемый «типом в штучной упаковке»). Места хранения, типы которых являются производными, System.ValueTypeбудут содержать первые; объекты кучи, чьи типы тоже делают, будут содержать последние. В большинстве языков расширяющийся состав существует от первого к последнему, и сужающийся состав от последнего к первому. Обратите внимание, что, хотя типы значений в штучной упаковке имеют тот же дескриптор типа, что и хранилища типов значений, ...
supercat

3

Это сводится к деталям реализации: типы значений реализуются иначе, чем ссылочные типы.

Если вы заставите типы значений обрабатываться как ссылочные типы (т. Е. Пометить их, например, путем обращения к ним через интерфейс), вы можете получить дисперсию.

Самый простой способ увидеть разницу - это просто рассмотреть Array: массив типов значений объединяется в памяти непрерывно (напрямую), где в качестве массива ссылочных типов имеется только ссылка (указатель) непрерывно в памяти; объекты, на которые указывают, выделяются отдельно.

Другая (связанная) проблема (*) заключается в том, что (почти) все ссылочные типы имеют одно и то же представление для целей дисперсии, и большой части кода не нужно знать о разнице между типами, поэтому возможна совместная и противоположная дисперсия (и легко реализовано - часто просто из-за отсутствия дополнительной проверки типов).

(*) Может быть, это та же проблема ...

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.