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

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

Функция имеет две перегрузки, та, которая берет Action, другие взятия a Func.

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

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

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

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

101
задан Richard Everett 26 March 2019 в 00:38
поделиться

4 ответа

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

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

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

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

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

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

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

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

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

97
ответ дан 24 November 2019 в 04:44
поделиться

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

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

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

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

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

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

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

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

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

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


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

Все еще взгляд...тем временем, если мы говорим, что "Eric Lippert" три раза, вы думаете, что мы получим посещение (и таким образом ответ)?

35
ответ дан 24 November 2019 в 04:44
поделиться

Перегрузка с FUNC и действие - это похожим (потому что они оба являются делегатами) к

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

void Function() // Action
{
}

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

0
ответ дан 24 November 2019 в 04:44
поделиться

Использование FUNC и Action (очевидно, очень отличается от действия и FUNC Строка> ) В классно сдается снимает двусмысленность.

Неоднозначность также происходит между действием и FUNC .

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

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(); }
} 
1
ответ дан 24 November 2019 в 04:44
поделиться
Другие вопросы по тегам:

Похожие вопросы: