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



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



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


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



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

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



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



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

865   3  

3 ответов:

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

  • классы (такие как List<T>) инвариантны; только делегаты и интерфейсы являются вариантными
  • чтобы дисперсия работала, интерфейс должен использовать параметр type только в одном направлении (in для контравариантности, out для ковариации)
  • типы значений не поддерживаются в качестве аргументов типа для дисперсии-поэтому нет никакого перехода от 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 действительно реализует этот интерфейс, и что проверка должна быть умной в распознавании дисперсии.

Но причина, по которой мы можем уйти с вариантными интерфейсами, являющимися просто преобразованиями no-op, заключается в , потому что регулярная совместимость назначений именно такова. Для чего вы собираетесь использовать Е2?

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. полный отстой, что ответ скита съедает все мои очки репутации. Будь ты проклят, скит! :- ) Похоже, что он ответил на это раньше , хотя.

Comments

    Ничего не найдено.