Неоднозначность в выводе типа параметра для лямбда-выражений С#

Мой вопрос мотивирован Эриком Липпертом в этом сообщении в блоге. Рассмотрим следующий код:

using System;
class Program {
    class A {}
    class B {}
    static void M(A x, B y) { Console.WriteLine("M(A, B)"); }
    static void Call(Action<A> f) { f(new A()); }
    static void Call(Action<B> f) { f(new B()); }
    static void Main() { Call(x => Call(y => M(x, y))); }
}

Это компилируется успешно и печатает M(A, B), потому что компилятор определяет, что типы x и y в лямбда-выражениях должны быть A и B соответственно. Теперь добавьте перегрузку для Program.M:

using System;
class Program {
    class A {}
    class B {}
    static void M(A x, B y) { Console.WriteLine("M(A, B)"); }
    static void M(B x, A y) { Console.WriteLine("M(B, A)"); } // added line
    static void Call(Action<A> f) { f(new A()); }
    static void Call(Action<B> f) { f(new B()); }
    static void Main() { Call(x => Call(y => M(x, y))); }
}

Это дает ошибку времени компиляции:

ошибка CS0121: вызов неоднозначен между следующими методами или свойствами: 'Program.Call(Action < Program.A > )' и 'Program.Call(Action < Program.B > )'

Компилятор не может вывести типы x и y. Возможно, что x имеет тип A, а y имеет тип B или наоборот, и ни одна из них не может быть предпочтительной из-за полной симметрии. Все идет нормально. Теперь добавьте еще одну перегрузку для Program.M:

using System;
class Program {
    class A {}
    class B {}
    static void M(A x, B y) { Console.WriteLine("M(A, B)"); }
    static void M(B x, A y) { Console.WriteLine("M(B, A)"); }
    static void M(B x, B y) { Console.WriteLine("M(B, B)"); } // added line
    static void Call(Action<A> f) { f(new A()); }
    static void Call(Action<B> f) { f(new B()); }
    static void Main() { Call(x => Call(y => M(x, y))); }
}

Эта команда успешно компилируется и печатает M(A, B) снова! Я могу угадать причину. Компилятор разрешает перегрузку Program.Call, пытаясь скомпилировать лямбда-выражение x => Call(y => M(x, y)) для x типа A и для x типа B. Первый успешно, в то время как последний терпит неудачу из-за двусмысленности, обнаруженной при попытке вывести тип y. Поэтому компилятор заключает, что x должен иметь тип A.

Таким образом, добавление большей двусмысленности приводит к меньшей двусмысленности. Это странно. Более того, это не соответствует тому, что написал Эрик в вышеупомянутом сообщении:

Если у него более одного решения, компиляция не выполняется с ошибкой двусмысленности.

Есть ли веская причина для текущего поведения? Это просто вопрос облегчения жизни компилятора? Или это скорее недостаток компилятора/спецификации?

Ответы

Ответ 1

Интересные сценарии. Рассмотрим, как компилятор анализирует каждый.

В вашем первом сценарии единственная возможность состоит в том, что x является A и y является B. Все остальное создает ошибку.

В вашем втором сценарии мы могли бы иметь x, A - это B, или x - это B, y равно A. Либо решение работает, мы не имеем оснований, чтобы предпочесть его, поэтому программа неоднозначна.

Теперь рассмотрим ваш третий сценарий. Начнем с предположения, что x равно B. Если x есть B, то y может быть A или B. У нас нет причин предпочитать A или B для y. Поэтому программа, в которой x есть B, неоднозначна. Следовательно, x не может быть B; наше предположение должно быть ошибочным.

Итак, либо x есть A, либо программа ошибочна. Может х быть А? Если это так, то y должно быть B. Мы не выводим ошибку, если x является A, и мы выводим ошибку, если x является B, поэтому x должно быть A.

Из этого можно сделать вывод, что x есть A и y есть B.

Это странно.

Угу. Разрешение перегрузки достаточно сложно в мире без вывода типового типа и лямбда. С ними это действительно довольно сложно.

Я подозреваю, что ваша трудность заключается в том, что кажется лучшим анализом для третьего сценария:

  • x - A, y - A не выполняется
  • x - это A, y - работа B
  • x - это B, y - A works
  • x - это B, y - это B работает
  • поэтому есть три решения, ни один не лучше, поэтому это неоднозначно.

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

Если вы сделаете аргумент, что это немного странно, что "порядок имеет значение" - лямбда снаружи в некотором смысле "привилегирована" над лямбдами внутри, ну, конечно, я могу видеть этот аргумент. Это характер алгоритмов обратного отслеживания.

Если у него более одного решения, компиляция не выполняется с ошибкой двусмысленности.

Это все еще так. В первом и третьем сценариях есть одно решение, которое можно вывести без противоречия; во втором - два решения и это двусмысленное.

Есть ли веская причина для текущего поведения?

Да. Мы рассматривали эти правила чрезвычайно тщательно. Как и все проектные решения, был достигнут компромисс.

Это просто вопрос облегчения жизни компилятора?

HA HA HA HA HA HA HA HA HA.

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

Или это скорее недостаток компилятора/спецификации?

Нет.

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