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

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

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)! Очевидно, что ни один из двух методов, участвующих в двусмысленности, не является новым добавленным методом, поскольку первый является параметром, а второй является общим.

Это ошибка? Какая часть спецификации говорит, что это должно быть так?

Ответ 1

Это ошибка?

Да.

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

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

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

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

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

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

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

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

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

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

Ответ 2

Какая часть спецификации говорит, что это должно быть так?

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

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

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

Ответ 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");
    }
}

Ответ 4

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

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

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

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

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

Ответ 5

  • Если вы пишете

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

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

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

    он будет в том же методе, в методе

    public void ComplexOverloadResolution(params string[] something
    

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

  • Если вы попытаетесь добавить новый метод yuor, подобный этому

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

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

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

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

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

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

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

    var blah = new Overloaded();
    blah.ComplexOverloadResolution(); // will be ComplexOverloadResolution(params string[] something) function called here, like a best match.