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

Мы только что нашли их в нашем коде:

public static class ObjectContextExtensions
{

    public static T Find<T>(this ObjectSet<T> set, int id, params Expression<Func<T, object>>[] includes) where T : class
    {
        ...
    }

    public static T Find<T>(this ObjectSet<T> set, int id, params string[] includes) where T : class
    {
       ...
    }
}

Как видите, они имеют одинаковую подпись, за исключением params.

И они используются несколькими способами, один из них:

DBContext.Users.Find(userid.Value); //userid being an int? (Nullable<int>)

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

Вопрос 1. Почему это не приводит к ошибке компиляции?

Вопрос 2. Почему компилятор C# разрешает вышеуказанный вызов первому методу?

Редактировать: поясняю, это C# 4.0, .Net 4.0, Visual Studio 2010.


person Federico Berasategui    schedule 07.01.2014    source источник
comment
В этом сообщении Эрика Липперта недавно обсуждалось, как разрешаются перегрузки: ericlippert.com/2013. /12/23/чем ближе, тем лучше   -  person Katie Kilian    schedule 07.01.2014
comment
@CharlieKilian: По памяти я не думаю, что обсуждал эту ситуацию, не так ли? Я не помню, чтобы параметры вообще упоминались, и я не уверен, как вы можете назвать один ближе, чем другой, если оба являются в основном пустыми массивами... Надеюсь, Эрик увидит это и просветит нас. Конечно хороший вопрос...   -  person Chris    schedule 07.01.2014
comment
Изменяет ли порядок определения функций то, какая из них вызывается? Если это так, то ближайший из них является ближайшим или первым.   -  person Felan    schedule 07.01.2014
comment
@Chris, честно говоря, это может не применяться напрямую. Но учитывая, что params можно полностью исключить из параметров вызова (см. здесь: msdn. microsoft.com/en-us/library/w5zay9db.aspx), то я приближался к значению «первый определенный».   -  person Katie Kilian    schedule 07.01.2014
comment
@CharlieKilian В этом сообщении блога ничего не говорится о порядке объявлений.   -  person BartoszKP    schedule 07.01.2014
comment
@DStanley Он конкретно показал, как он это называет, и это неоднозначно.   -  person Servy    schedule 07.01.2014
comment
Интересно, что скажет компилятор, когда вы удалите params и зададите оба значения по умолчанию для null?   -  person leppie    schedule 07.01.2014
comment
Забавный факт: несмотря на то, что это работает, Resharper 7.1 рассматривает это как ошибку.   -  person Yandros    schedule 07.01.2014
comment
Кажется, что он считает общий объект более близким в параметрах. Если оба являются общими или оба не являются общими, он жалуется на двусмысленность. Я не могу сказать вам, почему это так, но я подозреваю, что какая-то строка в спецификации где-то говорит, что так оно и есть.   -  person Chris    schedule 07.01.2014
comment
@CharlieKilian: Что ж, если бы вы попробовали это, вы бы увидели, что порядок объявления не имеет значения, и я был бы шокирован, если бы компилятор каждый раз говорил: «О, хорошо, вы объявили это первым, поэтому я проигнорирую двусмысленность». Как вы видели из статьи о разрешении, вы увидите, что он получает набор кандидатов и исключает возможности, пока не останется один, и если их больше одного, мы ожидаем возникновения исключения неоднозначного вызова. Таким образом, где-то явно существует правило, которое определяет одно как более близкое, чем другое, просто никто здесь (в настоящее время) не может сказать, что это такое.   -  person Chris    schedule 07.01.2014
comment
Связанный вопрос: stackoverflow.com/q/17390255/15541   -  person leppie    schedule 07.01.2014
comment
@Chris Вы правы, в статье не указана точная причина, по которой этот метод считается «ближе» - если это действительно то, что здесь происходит. Мне было бы интересно услышать точное правило.   -  person Katie Kilian    schedule 07.01.2014
comment
Посмотрите на ссылку IDEONE в моем ответе - мне это кажется ошибкой :)   -  person BartoszKP    schedule 07.01.2014


Ответы (3)


Это явно ошибка в разрешении перегрузки.

Он воспроизводится в C# 5 и C# 3, но не в Roslyn; Я не помню, намеренно ли мы решили взять мелочь или это случайность. (Сейчас на моей машине нет C# 4, но если он воспроизводится в 3 и 5, то почти наверняка будет и в 4.)

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

Поскольку у меня больше нет доступа к исходному коду C# 3/4/5, я не могу сказать, в чем причина ошибки. Сообщите об этом на сайте connect.microsoft.com.

Вот сильно упрощенное воспроизведение:

class P
{
    static void M(params System.Collections.Generic.List<string>[] p) {}
    static void M(params int[] p)  {}
    static void Main()
    {
        M();
    }
}

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

Ошибка, кстати, скорее всего, моя вина, так как я проделал изрядную часть работы над алгоритмом разрешения перегрузок в C# 3. Приносим извинения за ошибку.

ОБНОВИТЬ

Мои шпионы в команде Roslyn говорят мне, что это известная давняя ошибка в разрешении перегрузок. Было реализовано правило разрешения конфликтов, которое никогда не было задокументировано или обосновано и гласило, что тип с большой универсальной арностью является лучшим типом. Это странное правило без каких-либо оправданий, но оно никогда не удалялось из продукта. Некоторое время назад команда Roslyn решила внести критическое изменение и исправить разрешение перегрузки, чтобы в этом случае она выдавала ошибку. (Я не помню такого решения, но мы приняли много решений по такого рода вещам!)

person Eric Lippert    schedule 07.01.2014
comment
Спасибо за Ваш ответ. Это не доставило нам никаких хлопот, я спросил из любопытства. Я рад, что вношу свой вклад в улучшение языка/компилятора =) - person Federico Berasategui; 07.01.2014
comment
@HighCore: пожалуйста; благодарю Криса за то, что обратил на это мое внимание. Не вызовет ли у вас проблем, когда Roslyn вдруг начнет сообщать об этом как об ошибке при обновлении в будущем? - person Eric Lippert; 07.01.2014
comment
нет, я (и остальная часть моей команды тоже с этим согласны) предпочитаю, чтобы компилятор улавливал и устранял как можно больше двусмысленности. Мы можем легко исправить это, добавив перегрузку T Find<T>(this ObjectSet<T> set, int id){...} - person Federico Berasategui; 07.01.2014
comment
@EricLippert: Спасибо за отзыв. Постараюсь не просить вас о помощи. :) - person Chris; 08.01.2014

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

1) Набор методов-кандидатов для вызова метода построен. Начиная с набора методов, связанных с M, которые были найдены при предыдущем поиске элемента [...] Сокращение набора состоит в применении следующих правил к каждому методу TN в наборе, где T - это тип, в котором метод N объявляется:

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

Затем продолжается сокращение:

2) Если N неприменим в отношении A (Раздел 7.4.2.1), то N удаляется из набора.

Оба метода применимы в отношении Applicable function member правила в их расширенной форме:

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

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

Эксперименты (изменение типа параметра id на float в одном или обоих методах) подтверждают, что обе функции остаются в наборе кандидатов и дополнительно различаются по неявные правила сравнения конверсий .

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

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

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

person BartoszKP    schedule 07.01.2014
comment
Сбивает с толку то, что их расширенные формы должны быть абсолютно идентичными (он добавляет 0 параметров, поэтому их расширенные формы такие же, как вы уже отметили), поэтому я не понимаю, как это могло различаться после этого... - person Chris; 07.01.2014
comment
@Chris Посмотрите на ссылку IDEONE, которую я предоставил. Кажется, это отлично работает на MONO, так что это ошибка. - person BartoszKP; 07.01.2014
comment
Это определенно похоже на ошибку. Вопрос только в том, в каком. Лично я не верю, что смогу идеально прочитать спецификацию и весь ее сложный язык. ;-) Или, может быть, что-то изменилось, и мы сравниваем разные версии спецификаций... И запоздалый +1, так как я просто понял, что еще не сделал этого. - person Chris; 07.01.2014

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

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

То есть, если тип Expression<...> был изменен на int, компилятор будет жаловаться на двусмысленность. Аналогично, если оба типа являются общими, тогда он жалуется на двусмысленность.

Следующий фрагмент демонстрирует это поведение более просто:

void Main()
{
    TestMethod();
}

public void TestMethod(params string[] args)
{
    Console.WriteLine("NonGeneric");
}

public void TestMethod(params List<string>[] args)
{
    Console.WriteLine("Generic");
}

Это напечатает «Универсальный».

person Chris    schedule 07.01.2014
comment
Это противоречило бы спецификации C#.: If MP is a non-generic method and MQ is a generic method, then MP is better than MQ. - person Yandros; 07.01.2014
comment
У меня нет офиса на моем компьютере, поэтому у меня возникли проблемы с просмотром копии спецификации, но этот фрагмент проверен, а не выведен на основе догадок. Однако ваша цитата относится к универсальным методам, а не к универсальным параметрам методов. Также тот факт, что это должен быть параметр типа params, будет иметь значение где-то там... - person Chris; 07.01.2014
comment
Изменение типа параметра другой перегрузки на Action<T>, к сожалению, ничего не меняет. - person BartoszKP; 07.01.2014
comment
К сожалению, это на самом деле не отвечает на вопрос. - person ken2k; 07.01.2014
comment
@ken2k: Как я уже сказал в первом абзаце, я знаю, что это только половина ответа, но он, по крайней мере, отвечает, насколько он говорит. Он выбирает вариант с параметрами универсального типа, даже если это не объясняет, почему компилятор сделать это. - person Chris; 07.01.2014
comment
@Yandros: Это относится к универсальности метода, а не к типу формального параметра. То есть, если у нас есть конфликт между двумя методами M(int) и M<T>(T), где T есть int, то выигрывает не общий. - person Eric Lippert; 07.01.2014