Странный пример правил отклонения для делегатов

В сообщениях блога Эрика Липперта о ковариации и контравариантности или дисперсии для краткости, а также в таких книгах, как C # in a Nutshell, говорится, что:

Если вы определяете универсальный тип делегата, рекомендуется:

  • Отметьте параметр типа, используемый только для возвращаемого значения, как ковариантный (выходящий).
  • Отметьте любые параметры типа, используемые только для параметров, как контравариантные (in).

Это позволяет преобразованиям работать естественным образом, уважая отношения наследования между типами.

Я экспериментировал и нашел довольно странный пример.

Используя эту иерархию классов:

class Animal { }

class Mamal : Animal { }
class Reptile : Animal { }

class Dog : Mamal { }
class Hog : Mamal { }

class Snake : Reptile { }
class Turtle : Reptile { }

Пытаясь поиграть с преобразованиями групп методов в делегаты и преобразованиями делегатов в делегатов, я написал этот фрагмент кода:

 // Intellisense is complaining here  
 Func<Dog, Reptile> func1 = (Mamal d) => new Reptile();

 // A local method that has the same return type and same parameter type as the lambda expression.
 Reptile GetReptile(Mamal d) => new Reptile();

 // Works here.  
 Func<Dog, Reptile> func2 = GetReptile;

Почему правила дисперсии работают для локального метода, но не для лямбда-выражения?

Учитывая, что лямбда-выражение - это безымянный метод, написанный вместо экземпляра делегата, и что компилятор немедленно преобразует лямбда-выражение в одно из следующих значений:

  • Экземпляр делегата.
  • Дерево выражения типа Expression.

Я предполагаю, что с:

 Func<Dog, Reptile> func1 = (Mamal d) => new Reptile();

Происходит преобразование из чего-то вроде:

Func<Mamal, Reptile> => Func<Dog, Reptile>. 

Отличаются ли правила отклонения от делегатов к делегатам от правил отклонения для групп методов делегатам?


person Zack ISSOIR    schedule 28.02.2019    source источник


Ответы (1)


Позвольте мне немного пояснить ваш вопрос.

Эти три вещи могут быть преобразованы в тип делегата: (1) лямбда (или анонимный метод в стиле C # 2), (2) группа методов или локальный метод, (3) другой делегат. Различаются ли правила для ковариантных и контравариантных преобразований в каждом конкретном случае?

да.

Насколько они разные?

Вы должны прочитать спецификацию для точных деталей, но вкратце:

  • Универсальный тип делегата может быть преобразован в другой универсальный тип делегата , только если параметры типа делегата помечены как ковариантные или контравариантные. То есть Func<Giraffe> можно преобразовать в Func<Animal>, потому что Func<out T> помечен как ковариантный. (Кроме того: если вам нужно сделать вариантное преобразование из одного типа делегата в другой, а тип делегата не поддерживает вариативность, вы можете вместо этого использовать группу методов Invoke метода «исходного» делегата, и теперь мы Вы используете правила группы методов, но теряете ссылочное равенство.)

  • Группу методов или локальный метод можно преобразовать в соответствующий тип делегата с помощью правил ковариации и контравариантности, даже если делегат не помечен как поддерживающий дисперсию. То есть вы можете преобразовать Giraffe G() в delegate Animal D();, даже если D не является универсальным или является универсальным, но не помечен как вариант.

  • Правила преобразования лямбд сложны. Если лямбда не имеет формальных типов параметров, то используются формальные типы параметров целевого типа, тело лямбда анализируется, и оно может быть преобразовано, если тело анализируется без ошибок и результат совместим с тип результата целевого типа. Если лямбда-выражение действительно имеет типы формальных параметров, они должны точно соответствовать типам формальных параметров целевого типа.

Почему они разные?

Разные вещи разные. Я действительно не знаю, как ответить на такой расплывчатый, широкий вопрос «почему».

Эти правила были выведены дюжиной человек, сидящих в комнате на протяжении многих лет. Группа методов для делегирования преобразований была добавлена ​​в C # 1, универсальные делегаты были добавлены в C # 2, лямбда-выражения были добавлены в C # 3, универсальная дисперсия делегата была добавлена ​​в C # 4. Я не знаю, как ответить на вопрос «почему?» буквально сотни часов дизайнерской работы, и более половины из них было сделано до того, как я попал в команду дизайнеров. Эта проектная работа включала в себя множество аргументов и компромиссов. Пожалуйста, не задавайте расплывчатых вопросов «почему» и «почему нет» о дизайне языков программирования.

Вопросы типа "какая страница спецификации определяет это поведение?" есть ответ, но "почему в спецификации так сказано?" по сути, требует психологического анализа людей, которые занимались этим дизайном пятнадцать лет назад, и почему они нашли определенные компромиссы убедительными, а другие - не очень. Я не способен или не хочу проводить такой анализ; это потребовало бы перефразирования буквально сотен часов аргументов.

Если ваш вопрос: «Каковы общие принципы языкового дизайна, которые поощряют или препятствуют точному или неточному соответствию?» это тема, которую я мог бы подробно обсуждать часами. Например, я разработал новый алгоритм разрешения перегрузки вчера, и разрешение перегрузки - это не что иное, как определение того, когда важны точные или неточные совпадения и насколько они важны. Задайте более конкретный вопрос.

Вот что я вам скажу, давайте сделаем эту работу вы вместо меня. Вот один из ваших сценариев:

Action<Mammal> ma = (Animal a) => ...

Опишите мне неоспоримое преимущество запрета пользователю писать эту строку кода. Пример. Мне это действительно кажется ошибкой. Похоже, пользователь начал печатать что-то одно и на полпути передумал. Подобного рода бессмысленная, причудливая несогласованность очень характерна для небрежного, ошибочного кода, и ее можно легко предотвратить. Один из принципов разработки C # заключается в том, что язык сообщает вам, когда вы, вероятно, допустили ошибку. Это выглядит как ошибка.

Теперь приведите контраргумент, что код должен быть разрешен. Пример: также является общим принципом, что между лямбдами и локальными методами должна быть согласованность в том, что касается их правил конвертируемости? Насколько важно это правило по сравнению с правилом предотвращения небрежных ошибок?

А теперь приведите еще дюжину аргументов о тонких плюсах и минусах каждого варианта и о том, как различные сценарии разработчика влияют на ваш анализ каждого варианта. Приведите много-много примеров реалистичного кода.

Имейте в виду, что некоторые пользователи являются экспертами в системах типов, а некоторые нет. Некоторые из них архитекторы с двадцатилетним опытом, некоторые только что закончили колледж. Некоторые из них - программисты на Java, которые только вчера познакомились с C # и все еще настроены на стирание; некоторые из них - программисты на F #, привыкшие к полному программному выводу. Сделайте подробные заметки о плюсах и минусах каждого сценария, а затем придумайте компромиссное предложение, которое не приведет к слишком большим компромиссам ни по одному важному сценарию.

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

Приняв решение, опишите всю эту работу в одном предложении, которое отвечает на вопрос «почему вы так решили?»

person Eric Lippert    schedule 01.03.2019
comment
Извините за то, что этот вопрос помечен как ответ за долгое время. Может быть, то, как я задал этот вопрос, кажется, показывает, что я сомневаюсь в решениях команды дизайнеров, но это не тот случай, который я просто хотел понять, потому что я с большим энтузиазмом отношусь к этой теме. Я понимаю усилия, предпринятые для того, чтобы система типов работала правильно, и я также понимаю, что обсуждение и понимание решения требует глубоких знаний некоторых абстрактных математических тем. в любом случае, большое спасибо за то, что пролили свет на эту тему. - person Zack ISSOIR; 08.03.2019