Перегруженный аргумент метода-группы сбивает с толку разрешение перегрузки? - PullRequest
8 голосов
/ 05 марта 2011

Следующий вызов перегруженного метода Enumerable.Select:

var itemOnlyOneTuples = "test".Select<char, Tuple<char>>(Tuple.Create);

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

The call is ambiguous between the following methods or properties: 
'Enumerable.Select<char,Tuple<char>>
           (IEnumerable<char>,Func<char,Tuple<char>>)'
and 
'Enumerable.Select<char,Tuple<char>>
          (IEnumerable<char>, Func<char,int,Tuple<char>>)'

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

Мне кажется достаточно ясным, что намерение состоит в том, чтобы вызвать первую перегрузку, а аргумент метода-группы должен разрешиться до Tuple.Create<char>(char).Вторая перегрузка не должна применяться, поскольку ни одна из перегрузок Tuple.Create не может быть преобразована в ожидаемый тип Func<char,int,Tuple<char>>догадываюсь , что компилятор сбит с толку Tuple.Create<char, int>(char, int), но его тип возврата неверен: он возвращает двукратный кортеж и поэтому не может быть преобразован в соответствующий тип Func.

Кстати, любое из следующего делает компилятор счастливым:

  1. Указание аргумента типа для аргумента группы методов: Tuple.Create<char> (Возможно, это на самом деле проблема вывода типа?).
  2. Сделать аргумент лямбда-выражением вместо группы методов: x => Tuple.Create(x).(Хорошо играет с вводом типа при вызове Select).

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

var itemIndexTwoTuples = "test".Select<char, Tuple<char, int>>(Tuple.Create);

В чем здесь проблема?

Ответы [ 2 ]

20 голосов
/ 10 марта 2011

Прежде всего, отмечу, что это дубликат:

Почему Func неоднозначен с Func?

В чем здесь проблема?

Предположение Томаса, по сути, верно.Вот точные детали.

Давайте рассмотрим это шаг за шагом.У нас есть вызов:

"test".Select<char, Tuple<char>>(Tuple.Create); 

Разрешение перегрузки должно определять смысл вызова Select.Не существует метода «Выбор» для строки или какого-либо базового класса строки, поэтому это должен быть метод расширения.

Существует множество возможных методов расширения для набора кандидатов, поскольку строка преобразуется в IEnumerable<char>и, вероятно, где-то там есть using System.Linq;.Существует много методов расширения, которые соответствуют шаблону «Выбор, общая арность два, принимает IEnumerable<char> в качестве первого аргумента при построении с заданными аргументами типа метода».

В частности, два из кандидаты :

Enumerable.Select<char,Tuple<char>>(IEnumerable<char>,Func<char,Tuple<char>>)
Enumerable.Select<char,Tuple<char>>(IEnumerable<char>,Func<char,int,Tuple<char>>) 

Итак, первый вопрос, с которым мы сталкиваемся, это кандидаты применимо ?То есть существует ли неявное преобразование из каждого предоставленного аргумента в соответствующий тип формального параметра?

Отличный вопрос.Понятно, что первым аргументом будет «получатель», строка, и она будет неявно преобразована в IEnumerable<char>.Теперь вопрос заключается в том, является ли второй аргумент, группа методов «Tuple.Create», неявно конвертируемым в формальные типы параметров Func<char,Tuple<char>> и Func<char,int, Tuple<char>>.

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

То есть M преобразуется в Func<A, R>если бы разрешение перегрузки при вызове вида M(someA) было бы успешным, учитывая выражение 'someA' типа 'A'.

Было бы успешным разрешение перегрузки при вызове Tuple.Create(someChar)?Да;разрешение перегрузки выбрало бы Tuple.Create<char>(char).

Успешное разрешение перегрузки при вызове на Tuple.Create(someChar, someInt)?Да, для разрешения перегрузки было бы выбрано Tuple.Create<char,int>(char, int).

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

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

Поэтому у нас есть два подходящих кандидата.Есть ли способ, которым мы можем решить, который лучше , чем другой?В спецификации говорится, что преобразование в более конкретный тип лучше;если у вас есть

void M(string s) {}
void M(object o) {}
...
M(null);

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

Поэтому нет оснований отдавать предпочтение одному над другим.

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

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

Подобные ошибки неоднозначности легко построить. Например:

void M(Func<int, int> f){}
void M(Expression<Func<int, int>> ex) {}
...
M(x=>Q(++x));

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

Вы заметили, что

"test".Select<char, Tuple<char>>(Tuple.Create<char>); 

успешно. Теперь вы знаете, почему. Разрешение перегрузки должно определять, если

Tuple.Create<char>(someChar)

или

Tuple.Create<char>(someChar, someInt)

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

Вы также заметили, что

"test".Select<char, Tuple<char>>(x=>Tuple.Create(x)); 

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

5 голосов
/ 05 марта 2011

Я предполагаю, что компилятор сбит с толку Tuple.Create<char, int>(char, int), но его тип возврата неправильный: он возвращает двойную кортеж.

Тип возвращаемого значения не является частью сигнатуры метода, поэтому он не учитывается при разрешении перегрузки; проверяется только после перегрузки. Итак, насколько известно компилятору, Tuple.Create<char, int>(char, int) является действительным кандидатом, и он не лучше и не хуже, чем Tuple.Create<char>(char), поэтому компилятор не может принять решение.

...