Компілятор Неоднозначна помилка виклику - анонімний метод та група методів з функцією <> або дія


102

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

Функція має дві перевантаження: одна, яка приймає Action, інша приймає a 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 - це перелік виразів, кожен класифікований як змінна [...] відповідного параметра у формальній формі -параметр-список Д.

ГАРАЗД. Таким чином, ми робимо роздільну здатність перевантаження на 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).


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: 'string Program.GetString ()' має неправильний тип повернення

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


Стара відповідь видалена, окрім цього, тому що я думаю, що Ерік міг би пролити світло на "чому" цього питання ...

Ще дивимось ... середнього часу, якщо ми тричі скажемо "Ерік Ліпперт", ти вважаєш, що ми отримаємо візит (і, таким чином, відповідь)?


@Jon - чи могло це бути, classWithSimpleMethods.GetStringа classWithSimpleMethods.DoNothingне делегати?
Даніель А. Білий

@Daniel: Ні - ці вирази є виразами групи методів, і перевантажені методи слід вважати застосовними лише тоді, коли є неявна конверсія з групи методів у відповідний тип параметра. Див. Розділ 7.4.3.1 специфікації.
Джон Скіт

Читаючи розділ 6.6, схоже, що перетворення з classWithSimpleMethods.GetString в дію вважається існує, оскільки списки параметрів сумісні, але перетворення (у разі спроби) не вдалося під час компіляції. Таким чином, неявне перетворення дійсно існує для обох типів делегатів і виклик неоднозначний.
zinglon

@zinglon: Як ви читаєте §6.6, щоб визначити, що перехід від ClassWithSimpleMethods.GetStringдо Actionє дійсним? Для методу, Mсумісного з типом делегата D(§15.2), "ідентифікація або неявна опорна конверсія існує від типу повернення Mдо типу повернення D."
Ясон

@Jason: Специфікація не каже, що конверсія є дійсною, вона говорить, що вона існує . Насправді він недійсний, оскільки не працює під час компіляції. Перші два пункти §6.6 визначають, чи існує перетворення. Наступні пункти визначають, чи вдасться конверсія. З пункту 2: "В іншому випадку алгоритм створює єдиний найкращий метод M, що має таку ж кількість параметрів, що і D, і вважається, що перетворення існує". § 15.2 посилається на пункт 3.
Зінглон

1

Використання 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

Перевантаження Funcі Actionсхоже (тому що вони обоє є делегатами)

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

void Function() // Action
{
}

Якщо ви помітили, компілятор не знає, кому дзвонити, оскільки вони відрізняються лише типом повернення.


Я не думаю, що це насправді зовсім так - тому що ви не можете перетворити Func<string>в Action... і ви не можете перетворити групу методів, що складається тільки з методу, який повертає рядок в Actionбудь-який.
Джон Скіт

2
Ви не можете призначити делегата, який не має параметрів, і повертається stringдо Action. Я не бачу, чому є двозначність.
Язон

3
@dtb: Так, усунення перевантаження усуває проблему - але це насправді не пояснює, чому виникає проблема.
Джон Скіт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.