Вопрос

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

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

[Пример нефункционального:Смотрите ответ Джона Скита]

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

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

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

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

Это было полезно?

Решение

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

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

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

Итак, это бы работа:

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

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

Я считаю, что он не был представлен ранее, потому что разработчикам языка все еще нужно было проработать детали того, как его представить — он присутствует в CLR начиная с версии 2.

Преимущества такие же, как и в других случаях, когда вы хотите иметь возможность использовать один тип как другой.Если использовать тот же пример, который я использовал в прошлую субботу, если у вас есть что-то, что можно реализовать 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, ожидается расширенный тип базы (например IEnumerable<Base>) но все, что у вас есть, - это усиленный тип Производного (например IEnumerable<Derived>).

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top