Подтвердить что ты не робот

Почему компилятор не может указать лучшую цель преобразования в этом случае разрешения перегрузки? (Ковариация)

Понимание спецификации языка С# при разрешении перегрузки явно затруднено, и теперь мне интересно, почему этот простой случай терпит неудачу:

void Method(Func<string> f)
{
}
void Method(Func<object> f)
{
}
void Call()
{
    Method(() => { throw new NotSupportedException(); });
}

Это дает ошибку времени компиляции CS0121. Вызов неоднозначен между следующими методами или свойствами: за ним следуют два моих члена Method (перегрузки).

Я бы ожидал, что Func<string> был лучшей целью преобразования, чем Func<object>, а затем следует использовать первую перегрузку.

Начиная с .NET 4 и С# 4 (2010) общий тип делегата Func<out TResult> был ковариантным в TResult, и по этой причине существует неявное преобразование от Func<string> до Func<object>, хотя очевидно, что неявное преобразование может существовать от Func<object> до Func<string>. Таким образом, это сделает Func<string> лучшей целью преобразования, а разрешение перегрузки должно выбрать первую перегрузку?

Мой вопрос просто: какую часть С# Spec мне здесь не хватает?


Дополнение: Это отлично работает:

void Call()
{
    Method(null); // OK!
}
4b9b3361

Ответ 1

Мой вопрос просто: какую часть С# Spec мне здесь не хватает?

Резюме:

  • Вы обнаружили незначительную известную ошибку в реализации.
  • Ошибка будет сохранена по соображениям обратной совместимости.
  • Спецификация С# 3 содержала ошибку в отношении того, как должен обрабатываться "нулевой" случай; он был исправлен в спецификации С# 4.
  • Вы можете воспроизвести поведение багги с помощью любой лямбда, где тип возврата не может быть выведен. Например: Method(() => null);

Детали:

В спецификации С# 5 указано, что правило выдержки:

  • Если выражение имеет тип, выберите лучшее преобразование из этого типа в типы параметров-кандидата.

  • Если выражение не имеет тип и не является лямбдой, выберите преобразование в более подходящем типе.

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

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

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

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

Я отмечаю, что ошибка была практически невозможна, прежде чем я добавил дисперсию делегата в С# 4; в С# 3 невозможно, чтобы два разных типа делегатов были более или менее конкретными, поэтому единственным правилом, которое могло бы применяться, было правило лямбда. Поскольку в С# 3 не было теста, который бы выявил ошибку, ее было легко написать. Мой плохой, извините.

Я также отмечаю, что, когда вы начинаете бросать типы дерева выражений в микс, анализ становится еще более сложным. Даже если Func<string> лучше, чем Func<object>, Expression<Func<string>> не конвертируется в Expression<Func<object>>! Было бы неплохо, если бы алгоритм для betterness был агностиком в отношении того, лямбда направлялась к дереву выражений или делегату, но в некотором смысле это не так. Эти случаи осложняются, и я не хочу здесь трудиться.

Эта небольшая ошибка - это предметный урок в важности реализации того, что фактически говорит спецификация, а не того, что, по вашему мнению, она говорит. Если бы я был более осторожен в С# 3, чтобы убедиться, что код соответствует спецификации, тогда код будет сбой в случае с "нулевым", и тогда было бы ясно, что спецификация С# 3 была неправильной. И реализация делает проверку лямбды перед проверкой типа, которая была бомбой замедленного действия, ожидающей выхода, когда С# 4 скатывается, и вдруг это стало неправильным кодом. Проверка типа должна была быть сделана в первую очередь независимо.

Ответ 2

Ну, ты прав. Здесь возникает проблема с делегатом, который вы передаете в качестве аргумента. Он не имеет явного типа возврата, вы просто бросаете исключение. Exception - это в основном object, но он не рассматривается как возвращаемый тип метода. Поскольку при вызове исключения нет обратного вызова, компилятор не уверен, какую перегрузку он должен использовать.

Просто попробуйте это

void Call()
{
    Method(() => 
    { 
        throw new NotSupportedException();
        return "";
    });
}

Нет проблем с выбором перегрузки теперь из-за явно указанного типа объекта, переданного обратному вызову. Не имеет значения, что обратный вызов недоступен из-за исключения throw, но теперь компилятор знает, какую перегрузку он должен использовать.

EDIT:

Что касается случая с пропуском null, я не знаю ответа.