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


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>, код, що використовує послідовність, повинен знати, здійснювати перетворення боксу чи ні.

Ви можете прочитати публікацію блогу Еріка Ліпперта про представництво та особистість, щоб дізнатися більше про цю тему загалом.

EDIT: Перечитавши публікацію в блозі Еріка, мова йде про принаймні стільки ж про особистість, як і про представництво, хоча вони пов'язані між собою. Зокрема:

Ось чому коваріантні та противаріантні перетворення типів інтерфейсу та делегата вимагають, щоб усі аргументи різного типу були референсними типами. Щоб перетворення варіанту посилань завжди зберігало ідентичність, усі перетворення, що включають аргументи типу, також повинні зберігати ідентичність. Найпростіший спосіб переконатися, що всі нетривіальні перетворення на аргументах типів зберігають ідентичність - це обмежити їх перетвореннями посилань.


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мається на увазі структура, поля якої перекривають поля об’єкта купи, який зберігає його, а не посилається на об'єкт, який їх містить. Немає чистого способу для екземпляра типу коробчатого значення отримати посилання на об'єкт, що охоплює.
supercat

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);).
Андре Карон

Мова піклується про правильне лиття між типами, але поведінка ints не відповідає тій, яку ми очікували від підтипу об'єкта.
Тигран

Вся моя суть саме в тому, що intце не підтип object. Крім того, LSP не застосовується , тому що myInt, obj1і obj2відноситься до трьох різних об'єктів: один intі два (прихований) Integerс.
Андре Карон

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

@AllonGuralnek: Кожен тип значення (наприклад, System.Int32або List<String>.Enumerator) фактично представляє два види речей: тип місця зберігання та тип об’єкта купи (іноді його називають "типом коробки"). Місце зберігання, типи яких походять від, System.ValueTypeбуде містити перше; Купи об'єктів, типи яких так само, утримуватимуть останні. У більшості мов розширювальний відтінок існує від першого та останнього, а звуження від останнього до другого. Зауважте, що хоча типи значень у коробці мають той же дескриптор типу, як і місця зберігання типу значень, ...
supercat

3

Це зводиться до деталей реалізації: типи значень реалізуються по-різному до типів посилань.

Якщо ви змушуєте типи значень розглядатися як еталонні типи (тобто поле їх, наприклад, посилаючись на них через інтерфейс), ви можете отримати дисперсію.

Найпростіший спосіб побачити різницю - просто врахувати Array: масив типів значень об'єднується в пам'ять безперервно (безпосередньо), де як масив типів посилань має лише посилання (вказівник) безперервно в пам'яті; об'єкти, на які вказують, виділяються окремо.

Інший (пов'язаний) випуск (*) полягає в тому, що (майже) всі типи довідок мають однакове представлення для дисперсійних цілей і багато коду не потрібно знати різницю між типами, тому ко-та контра-дисперсія можлива (і легко) реалізовано - часто просто упущенням додаткової перевірки типу).

(*) Можливо, це те саме питання ...

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.