Зачем добавление метода добавляет неоднозначный вызов, если он не участвует в неоднозначности

112

У меня есть этот класс

public class Overloaded
{
    public void ComplexOverloadResolution(params string[] something)
    {
        Console.WriteLine("Normal Winner");
    }

    public void ComplexOverloadResolution<M>(M something)
    {
        Console.WriteLine("Confused");
    }
}

Если я назову это так:

        var blah = new Overloaded();
        blah.ComplexOverloadResolution("Which wins?");

Пишет Normal Winnerв консоль.

Но, если я добавлю еще один метод:

    public void ComplexOverloadResolution(string something, object somethingElse = null)
    {
        Console.WriteLine("Added Later");
    }

Я получаю следующую ошибку:

Вызов неоднозначен для следующих методов или свойств:> ' Overloaded.ComplexOverloadResolution(params string[])' и ' Overloaded.ComplexOverloadResolution<string>(string)'

Я понимаю, что добавление метода может привести к неоднозначности вызова, но это неоднозначность между двумя уже существующими методами (params string[])и <string>(string)! Очевидно, что ни один из двух методов, участвующих в неоднозначности, не является вновь добавленным методом, потому что первый - это параметры, а второй - общий.

Это ошибка? В какой части спецификации сказано, что так должно быть?

Маккей
источник
2
Я не думаю, что это 'Overloaded.ComplexOverloadResolution(string)'относится к <string>(string)методу; Я думаю, это относится к (string, object)методу без объекта.
phoog
1
@phoog О, эти данные были вырезаны StackOverflow, потому что это тег, но в сообщении об ошибке есть указатель шаблона. Я добавляю его обратно.
Маккей
ты поймал меня! В своем ответе я процитировал соответствующие разделы спецификации, но последние полчаса не потратил на их чтение и понимание!
phoog
@phoog, просматривая эти части спецификации, я ничего не вижу о введении двусмысленности в методы, кроме самого себя и другого метода, а не двух других методов.
McKay
Мне пришло в голову, что это всего лишь камень-ножницы-бумага : любой набор из двух различных значений имеет победителя, а полный набор из трех значений - нет.
phoog

Ответы:

107

Это ошибка?

Да.

Поздравляем, вы обнаружили ошибку в разрешении перегрузки. Ошибка воспроизводится в C # 4 и 5; не воспроизводится в версии семантического анализатора "Рослин". Я проинформировал команду тестирования C # 5, и, надеюсь, мы сможем исследовать эту проблему и решить ее до финального выпуска. (Как всегда, никаких обещаний.)

Далее следует правильный анализ. Кандидатами являются:

0: C(params string[]) in its normal form
1: C(params string[]) in its expanded form
2: C<string>(string) 
3: C(string, object) 

Нулевой кандидат явно неприменим, потому что stringне может быть преобразован в string[]. Остается три.

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

Что лучше: 1 или 2? Уместное решение проблемы заключается в том, что универсальный метод всегда хуже, чем неуниверсальный. 2 хуже 1. Значит, 2 не может быть победителем.

Что лучше: 1 или 3? Соответствующее решение для разрешения конфликтов: метод, применимый только в его развернутой форме, всегда хуже, чем метод, применимый в его нормальной форме. Следовательно, 1 хуже 3. Значит, 1 не может быть победителем.

Что лучше, 2 или 3? Уместное решение проблемы заключается в том, что универсальный метод всегда хуже, чем неуниверсальный. 2 хуже 3. Итак, 2 не может быть победителем.

Чтобы быть выбранным из множества подходящих кандидатов, кандидат должен быть (1) непобежденным, (2) превзойти хотя бы одного другого кандидата и (3) быть уникальным кандидатом, который имеет первые два свойства. Кандидат номер три не побежден ни одним другим кандидатом, и побеждает по крайней мере еще одного кандидата; это единственный кандидат с этим свойством. Следовательно, третий кандидат - единственный лучший кандидат . Он должен победить.

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

Можно резонно спросить, почему это так. Довольно сложно найти два метода, которые были бы «однозначно неоднозначными», потому что отношение «лучшего» непереходно . Возможны ситуации, когда кандидат 1 лучше 2, 2 лучше 3 и 3 лучше 1. В таких ситуациях мы не можем добиться большего, чем выбрать два из них как «неоднозначные».

Я хотел бы улучшить эту эвристику для Roslyn, но это низкий приоритет.

(Упражнение для читателя: «Разработайте алгоритм линейного времени для определения уникального лучшего члена набора из n элементов, в котором отношение лучшего является непереходным», - вот один из вопросов, который мне задали в день собеседования с этой командой. Это не очень сложный алгоритм; попробуйте.)

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

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

Спасибо, что обратили на это мое внимание. Приносим извинения за ошибку.

Эрик Липперт
источник
1
Спасибо за ответ. Вы сказали «1 хуже, чем 2», но он выбирает метод 1, если у меня есть только методы 1 и 2?
McKay
@McKay: Упс, вы правы, я сказал это задом наперед. Исправлю текст.
Эрик Липперт
1
Неловко читать фразу «остальная часть года», учитывая, что до конца не осталось и половины недели :)
BoltClock
2
@BoltClock действительно, выражение «уходить на оставшуюся часть года» подразумевает один выходной.
phoog
1
Я думаю так. Я прочитал «3) быть уникальным кандидатом, имеющим первые два свойства» как «быть единственным кандидатом, который (непобедим и превосходит хотя бы одного другого кандидата)» . Но ваш последний комментарий заставляет меня думать «(будь единственным кандидатом, который не побежден) и побьет хотя бы еще одного кандидата» . Английский действительно может использовать символы группировки. Если последнее верно, то я снова понимаю.
default.kramer
5

В какой части спецификации сказано, что так должно быть?

Раздел 7.5.3 (разрешение перегрузки), а также разделы 7.4 (поиск членов) и 7.5.2 (определение типа).

Особо обратите внимание на раздел 7.5.3.2 (улучшенный функциональный член), в котором частично говорится, что «необязательные параметры без соответствующих аргументов удаляются из списка параметров» и «Если M (p) - неуниверсальный метод, то M (q) - общий метод, то M (p) лучше, чем M (q) ».

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

фог
источник
Но это не объясняет, почему добавление члена вызовет двусмысленность между двумя уже существующими методами.
McKay
@McKay достаточно честно (см. Править). Нам просто нужно подождать, пока Эрик Липперт скажет нам, правильно ли это поведение: ->
phoog
1
Это правильные части спецификации. Беда в том, что говорят, что так быть не должно!
Эрик Липперт
3

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

как это :

public class Overloaded
{
    public void ComplexOverloadResolution(params string[] somethings)
    {
        Console.WriteLine("Normal Winner");
    }

    public void ComplexOverloadResolution<M>(M something)
    {
        Console.WriteLine("Confused");
    }

    public void ComplexOverloadResolution(string something, object somethingElse = null)
    {
        Console.WriteLine("Added Later");
    }
}

class Program
{
    static void Main(string[] args)
    {
        Overloaded a = new Overloaded();
        a.ComplexOverloadResolution(something:"asd");
    }
}
MhdSyrwan
источник
О, я знаю, что этот код плохой, и есть несколько способов обойти его, вопрос был в том, «почему компилятор ведет себя таким образом?»
McKay
1

Если вы удалите paramsиз своего первого метода, этого не произойдет. У первого и третьего метода есть оба действительных вызова ComplexOverloadResolution(string), но если у вас первый метод public void ComplexOverloadResolution(string[] something), двусмысленности не будет.

Предоставление значения для параметра object somethingElse = nullделает его необязательным параметром, и поэтому его не нужно указывать при вызове этой перегрузки.

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

'ConsoleApplication1.Program.ComplexOverloadResolution (строка параметров [])' и 'ConsoleApplication1.Program.ComplexOverloadResolution (строка, объект)'

Edit2: Новая находка. Удаление любого метода из трех приведенных выше не приведет к неоднозначности между ними. Таким образом, кажется, что конфликт возникает только при наличии трех методов, независимо от порядка.

Томислав Марковский
источник
Но это не объясняет, почему добавление члена вызовет двусмысленность между двумя уже существующими методами.
McKay
Неоднозначность возникает между вашим первым и третьим методом, но почему компилятор сообщает о двух других, я не понимаю.
Томислав Марковски
Но если я удалю второй метод, у меня нет двусмысленности, он успешно вызывает третий метод. Поэтому не похоже, что у компилятора есть двусмысленность между первым и третьим методами.
McKay
Смотрите мой Edit. Сумасшедшие компиляторы.
Томислав Марковски
Собственно, любая комбинация всего двух методов не вызывает двусмысленности. Это очень странно. Edit2.
Томислав Марковски
1
  1. Если вы напишете

    var blah = new Overloaded();
    blah.ComplexOverloadResolution("Which wins?");
    

    или просто напишите

    var blah = new Overloaded();
    blah.ComplexOverloadResolution();
    

    он попадет в тот же метод , в метод

    public void ComplexOverloadResolution(params string[] something

    Это paramsключевое слово cause, которое делает его наиболее подходящим также для случая, когда не указан параметр

  2. Если вы попытаетесь добавить новый метод вроде этого

    public void ComplexOverloadResolution(string something)
    {
        Console.WriteLine("Added Later");
    }
    

    Он отлично скомпилирует и вызовет этот метод, поскольку он идеально подходит для вашего вызова с stringпараметром. Тогда намного сильнее params string[] something.

  3. Вы объявляете второй метод, как и

    public void ComplexOverloadResolution(string something, object something=null);

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

    var blah = new Overloaded();
    blah.ComplexOverloadResolution("Which wins?");
    

    Фактически, если вы удалите строковый параметр из вызова, как следующий код, все компилируется правильно и работает, как раньше

    var blah = new Overloaded();
    blah.ComplexOverloadResolution(); // will be ComplexOverloadResolution(params string[] something) function called here, like a best match.
    
Тигран
источник
Сначала вы пишете два одинаковых варианта вызова. Так что, конечно, это будет один и тот же метод, или вы хотели написать что-то другое?
McKay
Но опять же, если я правильно понимаю остальную часть вашего ответа, вы не читаете то, что компилятор говорит о путанице, а именно между первым и вторым методами, а не новым третьим, который я только что добавил.
McKay
ах спасибо. Но это все еще оставляет проблему, о которой я упоминал во втором комментарии к вашему сообщению.
McKay
Для большей ясности вы заявляете: «Компилятор в полной путанице перепрыгивает между первым методом и этим, только что добавленным». Но это не так. Он в замешательстве переходит к двум другим методам. Метод params и универсальный метод.
McKay
@McKay: да, это сбивает с толку, имея state3 функции, а не одну или пару, если быть точным. На самом деле, достаточно прокомментировать любой из них, чтобы решить проблему. Лучшее совпадение среди доступных функций - это функция с params, вторая - функция с genericsпараметром, когда мы добавляем третью, это создает путаницу в одной setиз функций. Думаю, это, скорее всего, непонятное сообщение об ошибке компилятора.
Тигран