Ошибка неоднозначного вызова компилятора - анонимный метод и группа методов с Func <> или Action

102

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

Функция имеет две перегрузки: одна принимает Action, а другая - Func<string>.

Я могу с радостью вызвать две перегрузки, используя анонимные методы (или лямбда-синтаксис), но получаю ошибку компилятора неоднозначного вызова, если я использую синтаксис группы методов. Я могу обойти эту проблему путем явного приведения к Actionили Func<string>, но не думаю, что это необходимо.

Может ли кто-нибудь объяснить, почему требуется явное приведение.

Пример кода ниже.

class Program
{
    static void Main(string[] args)
    {
        ClassWithSimpleMethods classWithSimpleMethods = new ClassWithSimpleMethods();
        ClassWithDelegateMethods classWithDelegateMethods = new ClassWithDelegateMethods();

        // These both compile (lambda syntax)
        classWithDelegateMethods.Method(() => classWithSimpleMethods.GetString());
        classWithDelegateMethods.Method(() => classWithSimpleMethods.DoNothing());

        // These also compile (method group with explicit cast)
        classWithDelegateMethods.Method((Func<string>)classWithSimpleMethods.GetString);
        classWithDelegateMethods.Method((Action)classWithSimpleMethods.DoNothing);

        // These both error with "Ambiguous invocation" (method group)
        classWithDelegateMethods.Method(classWithSimpleMethods.GetString);
        classWithDelegateMethods.Method(classWithSimpleMethods.DoNothing);
    }
}

class ClassWithDelegateMethods
{
    public void Method(Func<string> func) { /* do something */ }
    public void Method(Action action) { /* do something */ }
}

class ClassWithSimpleMethods
{
    public string GetString() { return ""; }
    public void DoNothing() { }
}

C # 7.3 Обновление

Согласно комментарию 0xcde ниже 20 марта 2019 г. (через девять лет после того, как я разместил этот вопрос!), Этот код компилируется с C # 7.3 благодаря улучшенным кандидатам на перегрузку .

Ричард Ив
источник
Я пробовал ваш код и получаю дополнительную ошибку времени компиляции: 'void test.ClassWithSimpleMethods.DoNothing ()' имеет неправильный тип возврата (который находится в строке 25, где и возникла ошибка двусмысленности)
Мэтт Эллен
@Matt: Я тоже вижу эту ошибку. Ошибки, которые я привел в своем сообщении, были проблемами компиляции, которые VS выделяет еще до того, как вы даже попробуете полную компиляцию.
Ричард Эв,
1
Кстати, это был отличный вопрос. Мне нравится все, что заставляет меня работать над спецификациями :)
Джон Скит
1
Обратите внимание, что ваш пример кода будет компилироваться, если вы используете C # 7.3 ( <LangVersion>7.3</LangVersion>) или более позднюю версию, благодаря улучшенным кандидатам на перегрузку .
0xced

Ответы:

97

Во-первых, позвольте мне сказать, что ответ Джона правильный. Это одна из самых сложных частей спецификации, так что Джону так хорошо, что он сразу в нее погрузился.

Во-вторых, позвольте мне сказать, что эта строка:

Существует неявное преобразование из группы методов в совместимый тип делегата

(курсив наш) глубоко вводит в заблуждение и прискорбно. Я поговорю с Мэдсом об удалении слова «совместимый».

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

Теперь, когда мы разобрались с этим, мы можем пройти через раздел 6.6 спецификации и посмотреть, что мы получим.

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

class Program
{
    delegate void D1();
    delegate string D2();
    static string X() { return null; }
    static void Y(D1 d1) {}
    static void Y(D2 d2) {}
    static void Main()
    {
        Y(X);
    }
}

Итак, давайте рассмотрим это строка за строкой.

Существует неявное преобразование из группы методов в совместимый тип делегата.

Я уже обсуждал здесь неудачное слово «совместимый». Двигаемся дальше. Нам интересно, когда при разрешении перегрузки на Y (X) группа методов X преобразуется в D1? Он конвертируется в D2?

Учитывая тип делегата D и выражение E, которое классифицируется как группа методов, существует неявное преобразование из E в D, если E содержит хотя бы один метод, [...] применимый к списку аргументов, построенному с использованием параметра. типы и модификаторы D, как описано ниже.

Все идет нормально. X может содержать метод, применимый к спискам аргументов D1 или D2.

Применение во время компиляции преобразования из группы методов E в тип делегата D описывается ниже.

Эта строчка действительно ничего интересного не говорит.

Обратите внимание, что наличие неявного преобразования из E в D не гарантирует, что приложение времени компиляции преобразования будет успешным без ошибок.

Эта линия завораживает. Это означает, что существуют неявные преобразования, которые могут быть преобразованы в ошибки! Это странное правило C #. Чтобы отвлечься, приведем пример:

void Q(Expression<Func<string>> f){}
string M(int x) { ... }
...
int y = 123;
Q(()=>M(y++));

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

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

Двигаемся дальше:

Выбирается единственный метод M, соответствующий вызову метода формы E (A) [...] Список аргументов A - это список выражений, каждое из которых классифицируется как переменная [...] соответствующего параметра в формальном -список параметров D.

ХОРОШО. Таким образом, мы делаем разрешение перегрузки на X относительно D1. Список формальных параметров D1 пуст, поэтому мы выполняем разрешение перегрузки для X () и радости, мы находим метод "string X ()", который работает. Точно так же список формальных параметров D2 пуст. Опять же, мы обнаруживаем, что "string X ()" - это метод, который и здесь работает.

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

Если алгоритм [...] выдает ошибку, то возникает ошибка времени компиляции. В противном случае алгоритм создает единственный лучший метод M, имеющий то же количество параметров, что и D, и считается, что преобразование существует.

В группе методов X есть только один метод, поэтому он должен быть лучшим. Мы успешно доказали, что существует преобразование из X в D1 и из X в D2.

Актуальна ли эта строка?

Выбранный метод M должен быть совместим с типом делегата D, в противном случае возникает ошибка времени компиляции.

Собственно, нет, не в этой программе. Мы никогда не дойдем до активации этой линии. Потому что помните, что мы здесь делаем, пытаемся разрешить перегрузку по Y (X). У нас есть два кандидата Y (D1) и Y (D2). Оба применимы. Что лучше ? Нигде в спецификации мы не описываем различия между этими двумя возможными преобразованиями .

Конечно, можно утверждать, что действительное преобразование лучше, чем преобразование, приводящее к ошибке. В данном случае это фактически означало бы, что разрешение перегрузки ДЕЙСТВИТЕЛЬНО учитывает возвращаемые типы, чего мы хотим избежать. Тогда возникает вопрос, какой принцип лучше: (1) поддерживать инвариант, согласно которому разрешение перегрузки не учитывает возвращаемые типы, или (2) попытаться выбрать преобразование, которое, как мы знаем, будет работать, по сравнению с тем, которое, как мы знаем, не будет?

Это призыв к суждению. Что касается лямбда-выражений , мы действительно рассматриваем возвращаемый тип в этих видах преобразований в разделе 7.4.3.3:

E - анонимная функция, T1 и T2 - типы делегатов или типы дерева выражений с идентичными списками параметров, предполагаемый возвращаемый тип X существует для E в контексте этого списка параметров, и выполняется одно из следующих условий:

  • T1 имеет тип возврата Y1, а T2 имеет тип возврата Y2, и преобразование из X в Y1 лучше, чем преобразование из X в Y2.

  • T1 имеет возвращаемый тип Y, а T2 не возвращается

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

В любом случае, у нас нет правила «лучшего», чтобы определить, какое преобразование лучше, X в D1 или X в D2. Поэтому мы даем ошибку неоднозначности в разрешении Y (X).

Эрик Липперт
источник
8
Взлом - большое спасибо как за ответ, так и (надеюсь) за результирующее улучшение спецификации :) Лично я думаю, что для разрешения перегрузки было бы разумно учитывать тип возвращаемого значения для преобразований групп методов , чтобы сделать поведение более интуитивным, но Я понимаю, что это было бы сделано за счет последовательности. (То же самое можно сказать и о выводе общего типа применительно к преобразованиям групп методов, когда в группе методов есть только один метод, как я думаю, мы обсуждали ранее.)
Джон Скит,
35

РЕДАКТИРОВАТЬ: Я думаю, что у меня это есть.

Как говорит zinglon, это потому, что существует неявное преобразование из GetStringв, Actionдаже если приложение во время компиляции завершится с ошибкой. Вот введение в раздел 6.6 с некоторым (моим) акцентом:

Существует неявное преобразование (§6.1) из группы методов (§7.1) в совместимый тип делегата. Учитывая тип делегата D и выражение E, которое классифицируется как группа методов, существует неявное преобразование из E в D, если E содержит хотя бы один метод, который применим в его нормальной форме (§7.4.3.1) к составленному списку аргументов с использованием типов параметров и модификаторов D , как описано ниже.

Меня смутило первое предложение, в котором говорится о преобразовании в совместимый тип делегата. Actionне совместимый делегат для любого метода в GetStringгруппе методы, но GetString()метод является применимым в его нормальной форме список аргументов , построенный с использованием типов параметров и модификаторов D. Обратите внимание , что это не говорить о типе возвращаемого D. Вот почему он запутался ... потому что он будет проверять только совместимость делегата GetString()при применении преобразования, а не проверять его существование.

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

using System;

class Program
{
    static void ActionMethod(Action action) {}
    static void IntMethod(int x) {}

    static string GetString() { return ""; }

    static void Main(string[] args)
    {
        IntMethod(GetString);
        ActionMethod(GetString);
    }
}

Ни одно из выражений вызова метода в Mainкомпиляциях, но сообщения об ошибках разные. Вот для IntMethod(GetString):

Test.cs (12,9): ошибка CS1502: соответствие наилучшего перегруженного метода для 'Program.IntMethod (int)' имеет некоторые недопустимые аргументы

Другими словами, раздел 7.4.3.1 спецификации не может найти никаких применимых функциональных членов.

Теперь вот ошибка ActionMethod(GetString):

Test.cs (13,22): ошибка CS0407: строка Program.GetString () имеет неправильный тип возврата

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


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

Все еще ищу ... тем временем, если мы трижды скажем «Эрик Липперт», как вы думаете, нас посетят (и, следовательно, ответят)?

Джон Скит
источник
@Jon - может быть так classWithSimpleMethods.GetStringи classWithSimpleMethods.DoNothingне делегаты?
Дэниел А. Уайт,
@Daniel: Нет - эти выражения являются выражениями группы методов, и перегруженные методы следует считать применимыми только при неявном преобразовании из группы методов в соответствующий тип параметра. См. Раздел 7.4.3.1 спецификации.
Джон Скит,
Читая раздел 6.6, похоже, что преобразование из classWithSimpleMethods.GetString в Action считается существующим, поскольку списки параметров совместимы, но что преобразование (если оно было предпринято) не удается во время компиляции. Таким образом, неявное преобразование действительно существует для обоих типов делегатов и вызов неоднозначен.
zinglon
@zinglon: Как вы читаете §6.6, чтобы определить, действительно ли преобразование из ClassWithSimpleMethods.GetStringв Action? Чтобы метод Mбыл совместим с типом делегата D(§15.2), «существует идентификация или неявное преобразование ссылки из типа Mвозврата в тип возврата» D.
Джейсон
@Jason: В спецификации не говорится, что преобразование действительно, а говорится, что оно существует . Фактически, он недействителен, так как не работает во время компиляции. Первые два пункта §6.6 определяют, существует ли преобразование. Следующие пункты определяют, будет ли преобразование успешным. Из пункта 2: «В противном случае алгоритм создает единственный лучший метод M, имеющий то же количество параметров, что и D, и считается, что преобразование существует». §15.2 приводится в пункте 3.
zinglon
1

Использование Func<string>and Action<string>(очевидно, очень отличного от Actionand Func<string>) в ClassWithDelegateMethodsвыражении устраняет двусмысленность.

Неоднозначность также возникает между Actionи Func<int>.

Я также получаю ошибку двусмысленности с этим:

class Program
{ 
    static void Main(string[] args) 
    { 
        ClassWithSimpleMethods classWithSimpleMethods = new ClassWithSimpleMethods(); 
        ClassWithDelegateMethods classWithDelegateMethods = new ClassWithDelegateMethods(); 

        classWithDelegateMethods.Method(classWithSimpleMethods.GetOne);
    } 
} 

class ClassWithDelegateMethods 
{ 
    public void Method(Func<int> func) { /* do something */ }
    public void Method(Func<string> func) { /* do something */ } 
}

class ClassWithSimpleMethods 
{ 
    public string GetString() { return ""; } 
    public int GetOne() { return 1; }
} 

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

class Program
{
    static void Main(string[] args)
    {
        ClassWithSimpleMethods classWithSimpleMethods = new ClassWithSimpleMethods();
        ClassWithDelegateMethods classWithDelegateMethods = new ClassWithDelegateMethods();

        //The call is ambiguous between the following methods or properties: 
        //'test.ClassWithDelegateMethods.Method(System.Func<int,int>)' 
        //and 'test.ClassWithDelegateMethods.Method(test.ClassWithDelegateMethods.aDelegate)'
        classWithDelegateMethods.Method(classWithSimpleMethods.GetX);
    }
}

class ClassWithDelegateMethods
{
    public delegate string aDelegate(int x);
    public void Method(Func<int> func) { /* do something */ }
    public void Method(Func<string> func) { /* do something */ }
    public void Method(Func<int, int> func) { /* do something */ }
    public void Method(Func<string, string> func) { /* do something */ }
    public void Method(aDelegate ad) { }
}

class ClassWithSimpleMethods
{
    public string GetString() { return ""; }
    public int GetOne() { return 1; }
    public string GetX(int x) { return x.ToString(); }
} 
Мэтт Эллен
источник
0

Перегрузка с помощью Funcи Actionсродни (потому что они оба являются делегатами)

string Function() // Func<string>
{
}

void Function() // Action
{
}

Если вы заметили, компилятор не знает, какой из них вызывать, потому что они различаются только типами возвращаемых значений.

Дэниел А. Уайт
источник
Я не думаю, что это действительно так - потому что вы не можете преобразовать a Func<string>в Action... и вы не можете преобразовать группу методов, состоящую только из метода, который возвращает строку, в Actionлюбой.
Джон Скит,
2
Вы не можете привести делегата, не имеющего параметров и возвращающегося stringк Action. Не понимаю, почему возникает двусмысленность.
Джейсон
3
@dtb: Да, удаление перегрузки устраняет проблему, но на самом деле это не объясняет, почему существует проблема.
Джон Скит,