Общий разброс в С# 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>
или он просто построен в том, что теперь мы можем преобразовать между производными типами в родительские типы
Из интереса, почему это не было введено в предыдущих версиях и какое основное преимущество - то есть использование в реальном мире?
Дополнительная информация по этому сообщению для общей дисперсии (но вопрос крайне устарел, ища настоящую актуальную информацию)
Ответы
Ответ 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. Полностью сосет, что ответ Скита есть все мои реплики. Черт тебя, Скит!:-) Похоже, он ответил на это раньше.