Подтвердить что ты не робот

Общий разброс в С# 4.0

Общий разброс в С# 4.0 был реализован таким образом, что можно было написать следующее без исключения (что будет в С# 3.0):

 List<int> intList = new List<int>();
 List<object> objectList = intList; 

[Пример нерабочий: см. ответ Jon Skeet]

Недавно я присутствовал на конференции, где Джон Скит дал отличный обзор Generic Variance, но я не уверен, что полностью его понимаю - я понимаю значение ключевых слов in и out, когда это происходит для противоречия и со-дисперсии, но мне любопытно, что происходит за кулисами.

Что показывает CLR, когда этот код выполняется? Является ли он неявным преобразованием List<int> в List<object> или он просто построен в том, что теперь мы можем преобразовать между производными типами в родительские типы

Из интереса, почему это не было введено в предыдущих версиях и какое основное преимущество - то есть использование в реальном мире?

Дополнительная информация по этому сообщению для общей дисперсии (но вопрос крайне устарел, ища настоящую актуальную информацию)

4b9b3361

Ответ 1

Нет, ваш пример не будет работать по трем причинам:

  • Классы (такие как List<T>) являются инвариантными; только делегаты и интерфейсы являются вариантами
  • Для отклонения от работы интерфейс должен использовать параметр типа только в одном направлении (для контравариантности, для ковариации)
  • Типы значений не поддерживаются как аргументы типа для дисперсии - поэтому нет конверсии от IEnumerable<int> до IEnumerable<object>, например

(Код не скомпилирован как в С# 3.0, так и в 4.0 - нет исключения).

Итак, это сработает:

IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings;

CLR просто использует ссылку, неизменен - ​​новые объекты не создаются. Поэтому, если вы вызвали objects.GetType(), вы все равно получите List<string>.

Я полагаю, что это не было введено ранее, потому что языковым дизайнерам все еще приходилось разрабатывать детали того, как его выставить - это было в CLR с версии v2.

Преимущества такие же, как в другое время, когда вы хотите использовать один тип в качестве другого. Чтобы использовать тот же пример, который я использовал в прошлую субботу, если у вас есть что-то, реализующее IComparer<Shape> для сравнения фигур по областям, это безумие, что вы не можете использовать это для сортировки List<Circle> - если он может сравнивать любые две фигуры, он может, безусловно, сравнить любые два круга. Начиная с С# 4, было бы контравариантное преобразование от IComparer<Shape> до IComparer<Circle>, чтобы вы могли вызвать circles.Sort(areaComparer).

Ответ 2

Несколько дополнительных мыслей.

Что показывает CLR, когда этот код выполняется

Как правильно отметили Джон и другие, мы не делаем разброса по классам, только интерфейсам и делегатам. Итак, в вашем примере CLR ничего не видит; этот код не компилируется. Если вы вынудите его скомпилировать, вставив достаточное количество бросков, он выйдет из строя во время выполнения с плохим исключением исключения.

Теперь все еще резонный вопрос: как дисперсия работает за кулисами, когда она работает. Ответ таков: причина, по которой мы ограничиваем это аргументами ссылочного типа, которые параметризуют интерфейс и типы делегатов, так что ничего не происходит за кулисами. Когда вы скажете

object x = "hello";

что происходит за кулисами, ссылка на строку застревает в переменной объекта типа без изменения. Биты, составляющие ссылку на строку, являются законными битами для ссылки на объект, поэтому здесь ничего не должно произойти. CLR просто перестает думать о том, что эти биты относятся к строке, и начинает думать о них как о объекте.

Когда вы говорите:

IEnumerator<string> e1 = whatever;
IEnumerator<object> e2 = e1;

То же самое. Ничего не произошло. Биты, которые делают ref для перечислителя строк, совпадают с битами, которые делают ссылку на перечислитель объектов. Есть несколько больше волшебства, которое приходит в игру, когда вы делаете актерский состав, скажем:

IEnumerator<string> e1 = whatever;
IEnumerator<object> e2 = (IEnumerator<object>)(object)e1;

Теперь CLR должен сгенерировать проверку того, что e1 действительно реализует этот интерфейс, и эта проверка должна быть умной в распознавании дисперсии.

Но причина, по которой мы можем уйти с вариантными интерфейсами, являющимися просто конверсиями без операции, заключается в том, что обычная совместимость назначений такова. На что вы собираетесь использовать e2?

object z = e2.Current;

Это возвращает биты, которые являются ссылкой на строку. Мы уже установили, что они совместимы с объектом без изменений.

Почему раньше это не было введено? У нас были другие возможности и ограниченный бюджет.

Какой принцип выгоден? Это преобразование из последовательности строки в последовательность объекта "просто работать".

Ответ 3

Из интереса, почему это не было введенные в предыдущих версиях

В первых версиях (1.x).NET не было генериков вообще, поэтому общая дисперсия была далека.

Следует отметить, что во всех версиях .NET существует ковариация массива. К сожалению, это небезопасная ковариация:

Apple[] apples = new [] { apple1, apple2 };
Fruit[] fruit = apples;
fruit[1] = new Orange(); // Oh snap! Runtime exception! Can't store an orange in an array of apples!

Сопротиворечие в С# 4 является безопасным и предотвращает эту проблему.

какое основное преимущество - то есть реальное использование в мире?

Много раз в коде вы вызываете API, ожидаете усиленный тип Base (например, IEnumerable<Base>), но все, что у вас есть, - это усиленный тип Derived (например, IEnumerable<Derived>).

В С# 2 и С# 3 вам нужно будет вручную преобразовать в IEnumerable<Base>, хотя он должен "просто работать". Со-и противоречие - это просто "работа".

p.s. Полностью сосет, что ответ Скита есть все мои реплики. Черт тебя, Скит!:-) Похоже, он ответил на это раньше.