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

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

Функция имеет две перегрузки, одна из которых принимает 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 благодаря улучшенным кандидатам на перегрузку .

Ответы [ 4 ]

97 голосов
/ 13 января 2010

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

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

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

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

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

OK. Таким образом, мы делаем разрешение перегрузки на 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) попытаться выбрать преобразование, которое, как мы знаем, будет работать над тем, которое, как мы знаем, не будет?

Это призыв к суду. С lambdas мы do рассматриваем тип возвращаемого значения в этих видах преобразований, в разделе 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).

35 голосов
/ 13 января 2010

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

Как говорит 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, но я не вижу точно, где.


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

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

1 голос
/ 13 января 2010

Использование Func<string> и Action<string> (очевидно, сильно отличается от Action и 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 голосов
/ 13 января 2010

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

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

void Function() // Action
{
}

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

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