Общая дисперсия в C# 4.0
Общая дисперсия в C# 4.0 была реализована таким образом, что можно написать следующее без исключения (что происходит в C# 3.0):
List<int> intList = new List<int>();
List<object> objectList = intList;
[Пример не функционален: см. Ответ Джона Скита]
Недавно я присутствовал на конференции, на которой Джон Скит дал превосходный обзор Generic Variance, но я не уверен, что полностью его понимаю - я понимаю значение in
а также out
ключевые слова, когда дело доходит до противоречий и ко-дисперсии, но мне любопытно, что происходит за кулисами.
Что видит CLR при выполнении этого кода? Это неявное преобразование List<int>
в List<object>
или это просто встроено в то, что мы можем теперь преобразовывать между производными типами в родительские типы?
Из интереса, почему это не было введено в предыдущих версиях и каково главное преимущество - то есть использование в реальном мире?
Больше информации об этом посте для Generic Variance (но вопрос крайне устарел, в поисках реальной, актуальной информации)
3 ответа
Нет, ваш пример не сработает по трем причинам:
- Классы (такие как
List<T>
) инвариантны; только делегаты и интерфейсы являются вариантами - Чтобы дисперсия работала, интерфейс должен использовать параметр типа только в одном направлении (для контрастности, для ковариации)
- Типы значений не поддерживаются в качестве аргументов типа для дисперсии - поэтому нет разговоров с
IEnumerable<int>
вIEnumerable<object>
например
(Код не компилируется в C# 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>
- если он может сравнивать любые две фигуры, он, безусловно, может сравнивать любые две окружности. Начиная с C# 4, было бы контравариантное преобразование из IComparer<Shape>
в IComparer<Circle>
чтобы ты мог позвонить circles.Sort(areaComparer)
,
Несколько дополнительных мыслей.
Что видит CLR при выполнении этого кода
Как правильно отметили Джон и другие, мы не делаем различия между классами, только интерфейсами и делегатами. Так что в вашем примере CLR ничего не видит; этот код не компилируется. Если вы заставите его скомпилировать, вставив достаточное количество приведений, он завершится с ошибкой во время выполнения.
Теперь все еще разумно спросить, как работает дисперсия за кулисами, когда она работает. Ответ таков: причина, по которой мы ограничиваем это ссылочными аргументами типов, которые параметризуют интерфейсы и типы делегатов, заключается в том, что ничего не происходит за кулисами. Когда ты сказал
object x = "hello";
что происходит за кулисами - ссылка на строку вставляется в переменную типа object без изменения. Биты, составляющие ссылку на строку, являются допустимыми битами для ссылки на объект, поэтому здесь ничего не должно происходить. CLR просто перестает думать об этих битах как о ссылке на строку и начинает думать о них как об объекте.
Когда ты сказал:
IEnumerator<string> e1 = whatever;
IEnumerator<object> e2 = e1;
То же самое. Ничего не произошло. Биты, которые ссылаются на перечислитель строк, совпадают с битами, которые ссылаются на перечислитель объекта. Когда вы применяете актерский состав, в игру вступает нечто большее, скажем:
IEnumerator<string> e1 = whatever;
IEnumerator<object> e2 = (IEnumerator<object>)(object)e1;
Теперь CLR должен сгенерировать проверку того, что e1 действительно реализует этот интерфейс, и эта проверка должна быть умной для распознавания отклонений.
Но причина, по которой мы можем избавиться от вариантов интерфейсов, являющихся просто неоперативными преобразованиями, заключается в том, что регулярная совместимость присваиваний такова. Для чего вы собираетесь использовать e2?
object z = e2.Current;
Это возвращает биты, которые являются ссылкой на строку. Мы уже установили, что они совместимы с объектом без изменений.
Почему это не было введено ранее? У нас были другие возможности и ограниченный бюджет.
В чем принципиальная выгода? Это преобразование из последовательности строки в последовательность объекта "просто работа".
Из интереса, почему это не было введено в предыдущих версиях
В первых версиях (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!
Совместная и противоположная дисперсия в C# 4 безопасна и предотвращает эту проблему.
в чем главное преимущество - то есть использование в реальном мире?
Много раз в коде, при вызове API ожидается усиленный тип Base (например, IEnumerable<Base>
) но все, что у вас есть, это расширенный тип производного (например, IEnumerable<Derived>
).
В C# 2 и C# 3 вам нужно будет вручную преобразовать в IEnumerable<Base>
хотя это должно "просто работать". Совместная и противоположная дисперсия делает это "просто работой".
ps Полностью отстой, что ответ Скита съедает все мои очки репутации. Будь ты проклят, Скит!:-) Похоже, он уже ответил на это раньше.