Błąd niejednoznacznego wywołania kompilatora - metoda anonimowa i grupa metod z Func <> lub Action

102

Mam scenariusz, w którym chcę używać składni grupy metod zamiast metod anonimowych (lub składni lambda) do wywoływania funkcji.

Funkcja ma dwa przeciążenia, jedno przyjmuje wartość an Action, a drugie aFunc<string> .

Mogę szczęśliwie wywołać te dwa przeciążenia przy użyciu metod anonimowych (lub składni lambda), ale otrzymuję błąd kompilatora niejednoznacznego wywołania, jeśli używam składni grupy metod. Mogę obejść ten problem przez jawne rzutowanie na ActionlubFunc<string> ten problem , ale nie sądzę, by było to konieczne.

Czy ktoś może wyjaśnić, dlaczego wyraźne rzutowanie powinno być wymagane.

Przykład kodu poniżej.

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() { }
}

Aktualizacja C # 7.3

Zgodnie z komentarzem 0xcde poniżej z 20 marca 2019 r. (Dziewięć lat po tym, jak opublikowałem to pytanie!), Ten kod kompiluje się od wersji C # 7.3 dzięki ulepszonym kandydatom na przeciążenie .

Richard Ev
źródło
Próbowałem twojego kodu i otrzymuję dodatkowy błąd czasu kompilacji: `` void test.ClassWithSimpleMethods.DoNothing () '' ma nieprawidłowy typ zwrotu (który znajduje się w linii 25, gdzie jest błąd niejednoznaczności)
Matt Ellen
@Matt: Też widzę ten błąd. Błędy, które zacytowałem w moim poście, to problemy z kompilacją, które VS podkreśla, zanim jeszcze spróbujesz pełnej kompilacji.
Richard Ev,
1
Nawiasem mówiąc, to było świetne pytanie. Uwielbiam wszystko, co zmusza mnie do specyfikacji :)
Jon Skeet
1
Zwróć uwagę, że Twój przykładowy kod zostanie skompilowany, jeśli użyjesz C # 7.3 ( <LangVersion>7.3</LangVersion>) lub nowszego dzięki ulepszonym kandydatom na przeciążenie .
Wyświetlono

Odpowiedzi:

97

Po pierwsze, powiem tylko, że odpowiedź Jona jest poprawna. To jedna z najbardziej owłosionych części specyfikacji, tak dobra dla Jona, że ​​nurkuje w niej najpierw głową.

Po drugie, powiem, że ta linia:

Istnieje niejawna konwersja z grupy metod na zgodny typ delegata

(podkreślenie dodane) jest głęboko mylące i niefortunne. Porozmawiam z Madsem na temat usunięcia słowa „zgodny”.

Jest to mylące i niefortunne, ponieważ wygląda na to, że odwołuje się do sekcji 15.2, „Zgodność delegowania”. W sekcji 15.2 opisano relacje zgodności między metodami i typami delegatów , ale jest to kwestia konwersji grup metod i typów delegatów , która jest inna.

Skoro już to usunęliśmy, możemy przejść przez sekcję 6.6 specyfikacji i zobaczyć, co otrzymamy.

Aby rozwiązać problem, musimy najpierw określić, które przeciążenia są odpowiednimi kandydatami . Kandydat ma zastosowanie, jeśli wszystkie argumenty są niejawnie konwertowane na formalne typy parametrów. Rozważ tę uproszczoną wersję swojego programu:

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);
    }
}

Przejdźmy więc przez to linijka po linijce.

Istnieje niejawna konwersja z grupy metod na zgodny typ delegata.

Omówiłem już, że słowo „kompatybilny” jest tutaj niefortunne. Iść dalej. Zastanawiamy się, kiedy wykonujemy rozdzielczość przeciążenia na Y (X), czy grupa metod X konwertuje na D1? Czy konwertuje do D2?

Biorąc pod uwagę typ delegata D i wyrażenie E, które jest klasyfikowane jako grupa metod, istnieje niejawna konwersja z E na D, jeśli E zawiera co najmniej jedną metodę, która ma zastosowanie [...] do listy argumentów utworzonej przy użyciu parametru typy i modyfikatory D, jak opisano poniżej.

Na razie w porządku. X może zawierać metodę, która ma zastosowanie z listami argumentów D1 lub D2.

Zastosowanie konwersji w czasie kompilacji z grupy metod E do typu delegata D jest opisane poniżej.

Ta linia naprawdę nie mówi nic ciekawego.

Należy zauważyć, że istnienie niejawnej konwersji z E na D nie gwarantuje, że aplikacja konwersji w czasie kompilacji zakończy się pomyślnie bez błędu.

Ta linia jest fascynująca. Oznacza to, że istnieją niejawne konwersje, które istnieją, ale mogą zostać przekształcone w błędy! To jest dziwaczna reguła języka C #. Aby chwilę odejść, oto przykład:

void Q(Expression<Func<string>> f){}
string M(int x) { ... }
...
int y = 123;
Q(()=>M(y++));

Operacja inkrementacji jest niedozwolona w drzewie wyrażeń. Jednak lambda nadal można zamienić na typ drzewa wyrażenia, nawet jeśli konwersja jest kiedykolwiek używana, jest to błąd! Zasada jest taka, że ​​możemy chcieć później zmienić reguły dotyczące tego, co może znaleźć się w drzewie wyrażeń; zmiana tych reguł nie powinna zmieniać reguł systemu typów . Chcemy zmusić Cię do tego, aby Twoje programy były teraz jednoznaczne , więc kiedy w przyszłości zmienimy reguły drzew wyrażeń, aby były lepsze, nie wprowadzamy istotnych zmian w rozwiązywaniu przeciążeń .

W każdym razie jest to kolejny przykład tego rodzaju dziwacznej reguły. Konwersja może istnieć w celu rozwiązania problemu z przeciążeniem, ale w rzeczywistości może być błędem. Chociaż w rzeczywistości nie jest to dokładnie sytuacja, w której się tu znajdujemy.

Iść dalej:

Wybierana jest pojedyncza metoda M odpowiadająca wywołaniu metody w postaci E (A) [...] Lista argumentów A jest listą wyrażeń, z których każde jest klasyfikowane jako zmienna [...] odpowiedniego parametru w formalnej -lista-parametrów D.

DOBRZE. Więc wykonujemy rozdzielczość przeciążenia na X w odniesieniu do D1. Formalna lista parametrów D1 jest pusta, więc wykonujemy rozwiązanie przeciążenia na X () i joy, znajdujemy metodę „string X ()”, która działa. Podobnie formalna lista parametrów D2 jest pusta. Ponownie okazuje się, że „string X ()” jest metodą, która działa również tutaj.

Zasada jest taka, że określenie konwertowalności grupy metod wymaga wybrania metody z grupy metod przy użyciu rozpoznawania przeciążenia , a rozpoznawanie przeciążenia nie uwzględnia zwracanych typów .

Jeśli algorytm [...] generuje błąd, to występuje błąd w czasie kompilacji. W przeciwnym razie algorytm tworzy jedną najlepszą metodę M mającą taką samą liczbę parametrów jak D i uważa się, że konwersja istnieje.

Jest tylko jedna metoda w grupie metod X, więc musi być najlepsza. Udowodniliśmy, że istnieje konwersja z X na D1 iz X na D2.

Czy ta linia jest istotna?

Wybrana metoda M musi być zgodna z typem delegata D lub w przeciwnym razie wystąpi błąd w czasie kompilacji.

Właściwie nie, nie w tym programie. Nigdy nie osiągnęliśmy tak daleko, jak aktywacja tej linii. Ponieważ, pamiętaj, to, co tutaj robimy, to próba rozwiązania problemu z przeciążeniem na Y (X). Mamy dwóch kandydatów Y (D1) i Y (D2). Oba mają zastosowanie. Co jest lepsze ? Nigdzie w specyfikacji nie opisujemy lepkości między tymi dwoma możliwymi konwersjami .

Można by z pewnością argumentować, że poprawna konwersja jest lepsza niż taka, która powoduje błąd. W tym przypadku oznaczałoby to efektywnie stwierdzenie, że rozwiązanie przeciążenia ROZWAŻA zwracane typy, czego chcemy uniknąć. Powstaje zatem pytanie, która zasada jest lepsza: (1) zachowaj niezmiennik, zgodnie z którym rozwiązanie przeciążenia nie bierze pod uwagę typów zwracanych, czy (2) spróbuj wybrać konwersję, o której wiemy, że będzie działać na taką, o której wiemy, że nie?

To jest wezwanie do sądu. Z lambdas , możemy zrobić pod uwagę rodzaj powrotu w tego rodzaju konwersji w sekcji 7.4.3.3:

E to funkcja anonimowa, T1 i T2 to typy delegatów lub typy drzew wyrażeń z identycznymi listami parametrów, istnieje wywnioskowany zwracany typ X dla E w kontekście tej listy parametrów i jedna z następujących blokad:

  • T1 ma typ zwrotu Y1, a T2 ma typ zwrotu Y2, a konwersja z X na Y1 jest lepsza niż konwersja z X na Y2

  • T1 ma zwracany typ Y, a T2 jest zwracany jako void

Szkoda, że ​​konwersje grup metod i konwersje lambda są pod tym względem niespójne. Jednak mogę z tym żyć.

W każdym razie nie mamy zasady „lepszości”, która określałaby, która konwersja jest lepsza, X na D1 czy X na D2. Dlatego podajemy błąd niejednoznaczności przy rozdzielczości Y (X).

Eric Lippert
źródło
8
Pękanie - wielkie dzięki zarówno za odpowiedź, jak i (miejmy nadzieję) wynikającą z tego poprawę specyfikacji :) Osobiście uważam, że rozsądne byłoby, aby rozwiązanie przeciążenia uwzględniało zwracany typ konwersji grup metod , aby zachowanie było bardziej intuicyjne, ale Rozumiem, że zrobiłoby to kosztem spójności. (To samo można powiedzieć o wnioskowaniu o typie ogólnym, stosowanym do konwersji grup metod, gdy w grupie metod jest tylko jedna metoda, jak myślę, że omówiliśmy wcześniej.)
Jon Skeet
35

EDYCJA: Myślę, że mam to.

Jak mówi zinglon, dzieje się tak dlatego, że istnieje niejawna konwersja z GetStringna, Actionmimo że aplikacja w czasie kompilacji zakończyłaby się niepowodzeniem. Oto wprowadzenie do sekcji 6.6, z pewnym naciskiem (moim):

Istnieje niejawna konwersja (§6.1) z grupy metod (§7.1) do zgodnego typu delegata. Biorąc pod uwagę typ delegata D i wyrażenie E, które jest sklasyfikowane jako grupa metod, istnieje niejawna konwersja z E do D, jeśli E zawiera co najmniej jedną metodę, która ma zastosowanie w jej normalnej postaci (§ 7.4.3.1) do skonstruowanej listy argumentów przy użyciu typów parametrów i modyfikatorów D , jak opisano poniżej.

Teraz byłem zdezorientowany pierwszym zdaniem - które mówi o konwersji na zgodny typ delegata. Actionnie jest zgodnym delegatem dla żadnej metody w GetStringgrupie metod, ale GetString()metoda ma zastosowanie w swojej normalnej postaci do listy argumentów utworzonej przy użyciu typów parametrów i modyfikatorów D. Należy zauważyć, że nie dotyczy to zwracanego typu D. Dlatego jest zdezorientowany ... ponieważ sprawdzałby tylko zgodność delegata GetString()podczas stosowania konwersji, nie sprawdzając jej istnienia.

Myślę, że pouczające jest pozostawienie na krótko przeciążenia w równaniu i zobaczenie, jak ta różnica między istnieniem konwersji a jej stosowalnością może się przejawiać. Oto krótki, ale kompletny przykład:

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);
    }
}

Żadne z wyrażeń wywołania metody w Mainkompilacjach, ale komunikaty o błędach są różne. Oto jeden dla IntMethod(GetString):

Test.cs (12,9): błąd CS1502: dopasowanie najlepszej przeciążonej metody dla „Program.IntMethod (int)” zawiera nieprawidłowe argumenty

Innymi słowy, sekcja 7.4.3.1 specyfikacji nie może znaleźć żadnych odpowiednich elementów składowych funkcji.

Oto błąd dla ActionMethod(GetString):

Test.cs (13,22): błąd CS0407: „łańcuch Program.GetString ()” ma nieprawidłowy typ zwracanej wartości

Tym razem wypracował metodę, którą chce wywołać, ale nie udało się następnie wykonać wymaganej konwersji. Niestety nie mogę znaleźć fragmentu specyfikacji, w którym odbywa się ta ostateczna kontrola - wygląda na to, że może być w wersji 7.5.5.1, ale nie wiem dokładnie, gdzie.


Stara odpowiedź została usunięta, z wyjątkiem tego fragmentu - ponieważ spodziewam się, że Eric mógłby rzucić światło na „dlaczego” tego pytania ...

Wciąż szukam… W międzyczasie, jeśli powiemy „Eric Lippert” trzy razy, czy myślisz, że będziemy mieli wizytę (a tym samym odpowiedź)?

Jon Skeet
źródło
@Jon - czy to możliwe classWithSimpleMethods.GetStringi classWithSimpleMethods.DoNothingnie są delegatami?
Daniel A. White
@Daniel: Nie - te wyrażenia są wyrażeniami grupy metod, a przeciążone metody należy uznać za mające zastosowanie tylko wtedy, gdy istnieje niejawna konwersja z grupy metod na odpowiedni typ parametru. Patrz sekcja 7.4.3.1 specyfikacji.
Jon Skeet
Czytając sekcję 6.6, wygląda na to, że konwersja z classWithSimpleMethods.GetString do Action jest uważana za istniejącą, ponieważ listy parametrów są zgodne, ale konwersja (jeśli próbowano) nie powiedzie się w czasie kompilacji. Dlatego niejawna konwersja nie istnieją dla obu typów delegatów i wywołanie jest niejednoznaczna.
zinglon
@zinglon: Jak czytasz §6.6, aby stwierdzić, że konwersja z ClassWithSimpleMethods.GetStringna Actionjest ważna? Aby metoda Mbyła zgodna z typem delegata D(§15.2) „istnieje konwersja tożsamości lub niejawnej referencji z zwracanego typu z Mna zwracany typ D.”
jason
@Jason: Specyfikacja nie mówi, że konwersja jest prawidłowa, mówi, że istnieje . W rzeczywistości jest nieprawidłowy, ponieważ nie działa w czasie kompilacji. Pierwsze dwa punkty §6.6 określają, czy konwersja istnieje. Poniższe punkty określają, czy konwersja powiedzie się. Z punktu 2: „W przeciwnym razie algorytm tworzy jedną najlepszą metodę M mającą taką samą liczbę parametrów jak D i uznaje się, że konwersja istnieje”. §15.2 jest przywołany w punkcie 3.
zinglon
1

Użycie Func<string>i Action<string>(oczywiście bardzo różne od Actioni Func<string>) w ClassWithDelegateMethodselemencie usuwa niejednoznaczność.

Niejednoznaczność występuje również między Actiona Func<int>.

Otrzymuję również błąd niejednoznaczności z tym:

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; }
} 

Dalsze eksperymenty pokazują, że podczas przekazywania własnej grupy metod przez siebie zwracany typ jest całkowicie ignorowany podczas określania, które przeciążenie ma zostać użyte.

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(); }
} 
Matt Ellen
źródło
0

Przeciążenie z Funci Actionjest podobne (ponieważ oba są delegatami)

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

void Function() // Action
{
}

Jeśli zauważysz, kompilator nie wie, który z nich wywołać, ponieważ różnią się tylko typami zwracanymi.

Daniel A. White
źródło
Nie wydaje mi się, żeby tak było - ponieważ nie można przekonwertować a Func<string>na Action... i nie można przekonwertować grupy metod składającej się tylko z metody, która zwraca ciąg znaków na Actionobie.
Jon Skeet
2
Nie można rzutować delegata, który nie ma parametrów i wraca stringdo pliku Action. Nie rozumiem, dlaczego jest dwuznaczność.
jason
3
@dtb: Tak, usunięcie przeciążenia usuwa problem - ale to tak naprawdę nie wyjaśnia, dlaczego jest problem.
Jon Skeet