Общая дисперсия в C # 4.0 - PullRequest
       62

Общая дисперсия в C # 4.0

16 голосов
/ 05 февраля 2010

Общая дисперсия в 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> или он просто встроен в то, что теперь мы можем преобразовывать производные типы в родительские типы?

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

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

Ответы [ 3 ]

20 голосов
/ 05 февраля 2010

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

  • Классы (такие как 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).

15 голосов
/ 10 февраля 2010

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

Что видит 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;

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

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

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

8 голосов
/ 05 февраля 2010

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

В первых версиях (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>), но все, что у вас есть, - это расширенный тип Derived (например, IEnumerable<Derived>).

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

p.s. Полностью отстой, что ответ Скита съедает все мои очки репутации. Будь ты проклят, Скит! :-) Похоже, он ответил на это до , хотя.

...