Подтвердить что ты не робот

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

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

Функция имеет две перегрузки: одну, которая принимает 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() { }
}
4b9b3361

Ответ 1

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

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

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

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

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

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

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 пуст. Опять же, мы обнаруживаем, что "строка X()" - это метод, который также работает здесь.

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

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

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

Теперь, соответствует ли эта строка?

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

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

Теперь можно было бы утверждать, что правильное преобразование лучше, чем допустимое. Тогда было бы эффективно сказать, в этом случае, что разрешение перегрузки разрешает типы возврата, чего мы хотим избежать. Вопрос в том, какой принцип лучше: (1) поддерживать инвариант, что разрешение перегрузки не учитывает типы возврата, или (2) попытаться выбрать преобразование, которое мы знаем, будет работать над тем, что, как мы знаем, не будет?

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

Ответ 2

EDIT: Я думаю, что у меня есть.

Как говорит 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: 'string Ошибка Program.GetString() ' тип возврата

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


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

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

Ответ 3

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

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

void Function() // Action
{
}

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

Ответ 4

Использование 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(); }
}