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


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

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

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 и an выражение E, которое классифицируется как a группа методов, неявное преобразование существует от E до D, Если E содержит at крайней мере один метод, который применим [...] к список аргументов, построенный с помощью типы параметров и модификаторы 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-это список выражений, каждое из которых классифицируется как переменная [...] соответствующего параметра в формальный параметр-список Д.

ОК. Поэтому мы перегружаем разрешение на X относительно D1. Формальный список параметров D1 пуст, поэтому мы перегружаем разрешение на X() и joy, мы находим метод "string X ()", который работает. Аналогично, формальный список параметров D2 пуст. Опять же, мы обнаруживаем, что" string X () " - это метод, который работает и здесь.

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

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

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

теперь эта строка актуальна?

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

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

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

это решение. С лямбда, мы do рассмотрим тип возврата в этих видах преобразований, в разделе 7.4.3.3:

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

  • T1 имеет тип возвращаемого значения Y1 и Т2 имеет тип возвращаемого значения Х2 и преобразования от X до Y1 лучше, чем преобразование из X в Y2

  • Т1 имеет возвращаемого типа Г, и Т2 является недействительным, возврате

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

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

EDIT: я думаю, что я получил его.

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

неявное преобразование (§6.1) существует от группы методов (§7.1) до a совместимый тип делегата. Дали делегат типа D и выражение 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):

перегрузка с Func и Action сродни (потому что оба они делегаты)

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

void Function() // Action
{
}

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

используя 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(); }
}