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