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 Action
lubFunc<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 .
<LangVersion>7.3</LangVersion>
) lub nowszego dzięki ulepszonym kandydatom na przeciążenie .Odpowiedzi:
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:
(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:
Przejdźmy więc przez to linijka po linijce.
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?
Na razie w porządku. X może zawierać metodę, która ma zastosowanie z listami argumentów D1 lub D2.
Ta linia naprawdę nie mówi nic ciekawego.
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:
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:
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 .
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?
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:
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).
źródło
EDYCJA: Myślę, że mam to.
Jak mówi zinglon, dzieje się tak dlatego, że istnieje niejawna konwersja z
GetString
na,Action
mimo że aplikacja w czasie kompilacji zakończyłaby się niepowodzeniem. Oto wprowadzenie do sekcji 6.6, z pewnym naciskiem (moim):Teraz byłem zdezorientowany pierwszym zdaniem - które mówi o konwersji na zgodny typ delegata.
Action
nie jest zgodnym delegatem dla żadnej metody wGetString
grupie metod, aleGetString()
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ść delegataGetString()
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:
Żadne z wyrażeń wywołania metody w
Main
kompilacjach, ale komunikaty o błędach są różne. Oto jeden dlaIntMethod(GetString)
: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)
: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ź)?
źródło
classWithSimpleMethods.GetString
iclassWithSimpleMethods.DoNothing
nie są delegatami?ClassWithSimpleMethods.GetString
naAction
jest ważna? Aby metodaM
była zgodna z typem delegataD
(§15.2) „istnieje konwersja tożsamości lub niejawnej referencji z zwracanego typu zM
na zwracany typD
.”Użycie
Func<string>
iAction<string>
(oczywiście bardzo różne odAction
iFunc<string>
) wClassWithDelegateMethods
elemencie usuwa niejednoznaczność.Niejednoznaczność występuje również między
Action
aFunc<int>
.Otrzymuję również błąd niejednoznaczności z tym:
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.
źródło
Przeciążenie z
Func
iAction
jest podobne (ponieważ oba są delegatami)Jeśli zauważysz, kompilator nie wie, który z nich wywołać, ponieważ różnią się tylko typami zwracanymi.
źródło
Func<string>
naAction
... i nie można przekonwertować grupy metod składającej się tylko z metody, która zwraca ciąg znaków naAction
obie.string
do plikuAction
. Nie rozumiem, dlaczego jest dwuznaczność.