Compiler Mehrdeutiger Aufruffehler - anonyme Methode und Methodengruppe mit Func <> oder Action

102

Ich habe ein Szenario, in dem ich die Methodengruppensyntax anstelle anonymer Methoden (oder Lambda-Syntax) zum Aufrufen einer Funktion verwenden möchte.

Die Funktion hat zwei Überladungen, eine, die eine nimmt Action, die andere nimmt eine Func<string>.

Ich kann die beiden Überladungen gerne mit anonymen Methoden (oder Lambda-Syntax) aufrufen, erhalte jedoch einen Compilerfehler beim mehrdeutigen Aufruf, wenn ich die Methodengruppensyntax verwende. Ich kann dies durch explizites Casting an Actionoder umgehen Func<string>, denke aber nicht, dass dies notwendig sein sollte.

Kann jemand erklären, warum die expliziten Besetzungen erforderlich sein sollten.

Codebeispiel unten.

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 Update

Wie pro 0xcde ‚s Kommentar unten am 20. März 2019 (9 Jahre nachdem ich diese Frage gestellt!), Ist dieser Code kompiliert wie C # 7.3 dank verbesserter Überlastung Kandidaten .

Richard Ev
quelle
Ich habe Ihren Code ausprobiert und erhalte einen zusätzlichen Fehler bei der Kompilierung: 'void test.ClassWithSimpleMethods.DoNothing ()' hat den falschen Rückgabetyp (in Zeile 25, wo der Mehrdeutigkeitsfehler liegt)
Matt Ellen
@ Matt: Ich sehe diesen Fehler auch. Die Fehler, die ich in meinem Beitrag zitiert habe, waren die Kompilierungsprobleme, die VS hervorhebt, bevor Sie überhaupt eine vollständige Kompilierung versuchen.
Richard Ev
1
Das war übrigens eine gute Frage. Ich liebe alles, was mich in die Spezifikationen zwingt :)
Jon Skeet
1
Beachten Sie, dass Ihr Beispielcode kompiliert wird, wenn Sie C # 7.3 ( <LangVersion>7.3</LangVersion>) oder höher verwenden, dank verbesserter Überlastungskandidaten .
0xced

Antworten:

97

Lassen Sie mich zunächst nur sagen, dass Jons Antwort richtig ist. Dies ist einer der haarigsten Teile der Spezifikation, so gut für Jon, dass er mit dem Kopf voran in die Spezifikation eintaucht.

Zweitens, lassen Sie mich sagen, dass diese Zeile:

Es besteht eine implizite Konvertierung von einer Methodengruppe in einen kompatiblen Delegattyp

(Hervorhebung hinzugefügt) ist zutiefst irreführend und unglücklich. Ich werde mit Mads darüber sprechen, wie das Wort "kompatibel" hier entfernt wird.

Der Grund dafür ist irreführend und unglücklich, weil es so aussieht, als würde dies in Abschnitt 15.2, "Kompatibilität von Delegierten", beschrieben. In Abschnitt 15.2 wurde die Kompatibilitätsbeziehung zwischen Methoden und Delegatentypen beschrieben. Dies ist jedoch eine Frage der Konvertierbarkeit von Methodengruppen und Delegattypen , die unterschiedlich ist.

Nachdem wir das aus dem Weg geräumt haben, können wir Abschnitt 6.6 der Spezifikation durchgehen und sehen, was wir bekommen.

Um eine Überlastungsauflösung durchzuführen, müssen wir zuerst bestimmen, welche Überlastungen geeignete Kandidaten sind . Ein Kandidat ist anwendbar, wenn alle Argumente implizit in die formalen Parametertypen konvertierbar sind. Betrachten Sie diese vereinfachte Version Ihres Programms:

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

Gehen wir es also Zeile für Zeile durch.

Es besteht eine implizite Konvertierung von einer Methodengruppe in einen kompatiblen Delegattyp.

Ich habe bereits besprochen, wie unglücklich das Wort "kompatibel" hier ist. Weitermachen. Wir fragen uns, ob bei der Überlastungsauflösung für Y (X) die Methodengruppe X in D1 konvertiert wird. Konvertiert es in D2?

Bei einem Delegatentyp D und einem Ausdruck E, der als Methodengruppe klassifiziert ist, besteht eine implizite Konvertierung von E nach D, wenn E mindestens eine Methode enthält, die [...] auf eine unter Verwendung des Parameters erstellte Argumentliste anwendbar ist Typen und Modifikatoren von D, wie im Folgenden beschrieben.

So weit, ist es gut. X kann eine Methode enthalten, die auf die Argumentlisten von D1 oder D2 anwendbar ist.

Die Anwendung einer Konvertierung von einer Methodengruppe E zu einem Delegatentyp D zur Kompilierungszeit wird im Folgenden beschrieben.

Diese Zeile sagt wirklich nichts Interessantes.

Beachten Sie, dass das Vorhandensein einer impliziten Konvertierung von E nach D nicht garantiert, dass die Anwendung der Konvertierung zur Kompilierungszeit ohne Fehler erfolgreich ist.

Diese Linie ist faszinierend. Es bedeutet, dass es implizite Konvertierungen gibt, die jedoch in Fehler umgewandelt werden können! Dies ist eine bizarre Regel von C #. Um einen Moment abzuschweifen, hier ein Beispiel:

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

Eine Inkrementierungsoperation ist in einem Ausdrucksbaum unzulässig. Das Lambda kann jedoch weiterhin in den Ausdrucksbaumtyp konvertiert werden , auch wenn die Konvertierung jemals verwendet wird, handelt es sich um einen Fehler! Das Prinzip hier ist, dass wir möglicherweise die Regeln ändern möchten, die später in einen Ausdrucksbaum aufgenommen werden können. Durch Ändern dieser Regeln sollten die Typsystemregeln nicht geändert werden . Wir möchten Sie zwingen, Ihre Programme jetzt eindeutig zu gestalten , damit wir keine Änderungen an der Überlastungsauflösung einführen , wenn wir die Regeln für Ausdrucksbäume in Zukunft ändern, um sie zu verbessern .

Auf jeden Fall ist dies ein weiteres Beispiel für diese Art von bizarrer Regel. Eine Konvertierung kann zum Zwecke der Überlastungsauflösung vorhanden sein, ist jedoch ein Fehler bei der tatsächlichen Verwendung. Tatsächlich ist dies jedoch nicht genau die Situation, in der wir uns befinden.

Weiter geht's:

Eine einzelne Methode M wird entsprechend einem Methodenaufruf der Form E (A) ausgewählt. [...] Die Argumentliste A ist eine Liste von Ausdrücken, die jeweils als Variable [...] des entsprechenden Parameters im Formal klassifiziert sind -Parameter-Liste von D.

OK. Wir überlasten also X in Bezug auf D1. Die formale Parameterliste von D1 ist leer, daher überladen wir X () und freuen uns, dass wir eine Methode "string X ()" finden, die funktioniert. Ebenso ist die formale Parameterliste von D2 leer. Wieder finden wir, dass "string X ()" eine Methode ist, die auch hier funktioniert.

Das Prinzip hierbei ist, dass zur Bestimmung der Konvertierbarkeit von Methodengruppen die Auswahl einer Methode aus einer Methodengruppe mithilfe der Überlastauflösung erforderlich ist und bei der Überlastauflösung keine Rückgabetypen berücksichtigt werden .

Wenn der Algorithmus [...] einen Fehler erzeugt, tritt ein Fehler bei der Kompilierung auf. Andernfalls erzeugt der Algorithmus eine einzelne beste Methode M mit der gleichen Anzahl von Parametern wie D, und die Umwandlung wird als vorhanden angesehen.

Es gibt nur eine Methode in der Methodengruppe X, daher muss es die beste sein. Wir haben erfolgreich bewiesen, dass eine Konvertierung vorliegt aus X zu D1 und D2 von X nach.

Ist diese Zeile nun relevant?

Die ausgewählte Methode M muss mit dem Delegatentyp D kompatibel sein. Andernfalls tritt ein Fehler bei der Kompilierung auf.

Eigentlich nein, nicht in diesem Programm. Wir kommen nie so weit, diese Linie zu aktivieren. Denken Sie daran, dass wir hier versuchen, eine Überlastungsauflösung für Y (X) durchzuführen. Wir haben zwei Kandidaten Y (D1) und Y (D2). Beides gilt. Welches ist besser ? Nirgendwo in der Spezifikation beschreiben wir die Verbesserung zwischen diesen beiden möglichen Konvertierungen .

Nun könnte man sicherlich argumentieren, dass eine gültige Konvertierung besser ist als eine, die einen Fehler erzeugt. Das würde dann effektiv bedeuten, dass in diesem Fall die Überlastungsauflösung Rückgabetypen berücksichtigt, was wir vermeiden möchten. Die Frage ist dann, welches Prinzip besser ist: (1) Behalten Sie die Invariante bei, dass die Überlastungsauflösung keine Rückgabetypen berücksichtigt, oder (2) versuchen Sie, eine Konvertierung auszuwählen, von der wir wissen, dass sie über eine Konvertierung funktioniert, von der wir wissen, dass sie nicht funktioniert.

Dies ist ein Urteilsspruch. Mit lambda , wir tun den Rückgabetyp in dieser Art von Umsetzungen betrachten, in Abschnitt 7.4.3.3:

E ist eine anonyme Funktion, T1 und T2 sind Delegiertypen oder Ausdrucksbaumtypen mit identischen Parameterlisten, ein abgeleiteter Rückgabetyp X existiert für E im Kontext dieser Parameterliste und einer der folgenden Punkte gilt:

  • T1 hat einen Rückgabetyp Y1 und T2 hat einen Rückgabetyp Y2, und die Konvertierung von X nach Y1 ist besser als die Konvertierung von X nach Y2

  • T1 hat einen Rückgabetyp Y und T2 ist nichtig

Es ist bedauerlich, dass Methodengruppen- und Lambda-Konvertierungen in dieser Hinsicht inkonsistent sind. Ich kann jedoch damit leben.

Wie auch immer, wir haben keine "Betterness" -Regel, um zu bestimmen, welche Umwandlung besser ist, X zu D1 oder X zu D2. Daher geben wir einen Mehrdeutigkeitsfehler bei der Auflösung von Y (X) an.

Eric Lippert
quelle
8
Cracking - vielen Dank sowohl für die Antwort als auch (hoffentlich) für die daraus resultierende Verbesserung der Spezifikation :) Persönlich denke ich, dass es für die Überlastungsauflösung vernünftig wäre, den Rückgabetyp für Methodengruppenkonvertierungen zu berücksichtigen , um das Verhalten intuitiver zu gestalten, aber Ich verstehe, dass dies auf Kosten der Konsistenz geschehen würde. (Dasselbe gilt für die generische Typinferenz, die auf Methodengruppenkonvertierungen angewendet wird, wenn es nur eine Methode in der Methodengruppe gibt, wie wir bereits besprochen haben.)
Jon Skeet
35

EDIT: Ich denke, ich habe es.

Wie Zinglon sagt, liegt dies daran, dass eine implizite Konvertierung von GetStringnach erfolgt Action, obwohl die Anwendung zur Kompilierungszeit fehlschlagen würde. Hier ist die Einführung zu Abschnitt 6.6 mit einigen Schwerpunkten (meiner):

Es besteht eine implizite Konvertierung (§6.1) von einer Methodengruppe (§7.1) in einen kompatiblen Delegatentyp. Bei einem Delegatentyp D und einem Ausdruck E, der als Methodengruppe klassifiziert ist, liegt eine implizite Konvertierung von E nach D vor, wenn E mindestens eine Methode enthält, die in ihrer normalen Form (§7.4.3.1) auf eine erstellte Argumentliste anwendbar ist unter Verwendung der Parametertypen und Modifikatoren von D , wie im Folgenden beschrieben.

Jetzt war ich durch den ersten Satz verwirrt, der von einer Konvertierung in einen kompatiblen Delegatentyp spricht. Actionist kein kompatibler Delegat für eine Methode in der GetStringMethodengruppe, aber die GetString()Methode ist in ihrer normalen Form auf eine Argumentliste anwendbar, die unter Verwendung der Parametertypen und Modifikatoren von D erstellt wurde. Beachten Sie, dass dies nicht über den Rückgabetyp von spricht D. Deshalb wird es verwirrt ... weil es nur die Kompatibilität der Delegierten GetString()beim Anwenden der Konvertierung überprüft und nicht auf ihre Existenz überprüft.

Ich denke, es ist lehrreich, die Überlastung kurz aus der Gleichung herauszulassen und zu sehen, wie sich dieser Unterschied zwischen der Existenz einer Konvertierung und ihrer Anwendbarkeit manifestieren kann. Hier ist ein kurzes, aber vollständiges Beispiel:

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

Keiner der Methodenaufrufausdrücke in MainKompilierungen, aber die Fehlermeldungen sind unterschiedlich. Hier ist der für IntMethod(GetString):

Test.cs (12,9): Fehler CS1502: Die beste überladene Methodenübereinstimmung für 'Program.IntMethod (int)' enthält einige ungültige Argumente

Mit anderen Worten, Abschnitt 7.4.3.1 der Spezifikation kann keine zutreffenden Funktionselemente finden.

Hier ist der Fehler für ActionMethod(GetString):

Test.cs (13,22): Fehler CS0407: 'string Program.GetString ()' hat den falschen Rückgabetyp

Dieses Mal hat es die Methode ausgearbeitet, die es aufrufen möchte - aber es ist nicht gelungen, die erforderliche Konvertierung durchzuführen. Leider kann ich nicht herausfinden, wo diese letzte Prüfung durchgeführt wird - es sieht so aus, als wäre es in 7.5.5.1, aber ich kann nicht genau sehen, wo.


Alte Antwort entfernt, bis auf dieses bisschen - weil ich davon ausgehe, dass Eric das "Warum" dieser Frage beleuchten könnte ...

Sie suchen immer noch ... in der Zwischenzeit, wenn wir dreimal "Eric Lippert" sagen, denken Sie, wir werden einen Besuch bekommen (und damit eine Antwort)?

Jon Skeet
quelle
@ Jon - könnte es das sein classWithSimpleMethods.GetStringund classWithSimpleMethods.DoNothingsind keine Delegierten?
Daniel A. White
@ Daniel: Nein - diese Ausdrücke sind Methodengruppenausdrücke, und die überladenen Methoden sollten nur dann als anwendbar angesehen werden, wenn eine implizite Konvertierung von der Methodengruppe in den relevanten Parametertyp erfolgt. Siehe Abschnitt 7.4.3.1 der Spezifikation.
Jon Skeet
Beim Lesen von Abschnitt 6.6 sieht es so aus, als ob die Konvertierung von classWithSimpleMethods.GetString in Action als vorhanden angesehen wird, da die Parameterlisten kompatibel sind, die Konvertierung (falls versucht) jedoch zum Zeitpunkt der Kompilierung fehlschlägt. Daher ist es eine implizite Konvertierung ist für beide Arten Delegierten bestehen , und der Anruf ist nicht eindeutig.
Zinglon
@zinglon: Wie liest du §6.6, um festzustellen, ob eine Konvertierung von ClassWithSimpleMethods.GetStringnach Actiongültig ist? Damit eine Methode Mmit einem Delegatentyp kompatibel ist D(§15.2), "existiert eine Identitäts- oder implizite Referenzkonvertierung vom Rückgabetyp Min den Rückgabetyp von D."
Jason
@ Jason: Die Spezifikation sagt nicht, dass die Konvertierung gültig ist, sie sagt, dass sie existiert . Tatsächlich ist es ungültig, da es beim Kompilieren fehlschlägt. Die ersten beiden Punkte von §6.6 bestimmen, ob die Umwandlung vorliegt. Die folgenden Punkte bestimmen, ob die Konvertierung erfolgreich sein wird. Aus Punkt 2: "Andernfalls erzeugt der Algorithmus eine einzige beste Methode M mit der gleichen Anzahl von Parametern wie D, und die Umwandlung wird als vorhanden angesehen." §15.2 wird in Punkt 3 aufgerufen.
Zinglon
1

Die Verwendung von Func<string>und Action<string>(offensichtlich sehr unterschiedlich zu Actionund Func<string>) in ClassWithDelegateMethodsentfernt die Mehrdeutigkeit.

Die Mehrdeutigkeit tritt auch zwischen Actionund auf Func<int>.

Ich bekomme auch den Mehrdeutigkeitsfehler damit:

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

Weitere Experimente zeigen, dass bei der Übergabe einer Methodengruppe an sich der Rückgabetyp bei der Bestimmung der zu verwendenden Überladung vollständig ignoriert wird.

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
quelle
0

Die Überladung mit Funcund Actionist verwandt (weil beide Delegierte sind) mit

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

void Function() // Action
{
}

Wenn Sie bemerken, weiß der Compiler nicht, welchen er aufrufen soll, da sie sich nur durch Rückgabetypen unterscheiden.

Daniel A. White
quelle
Ich denke nicht, dass es wirklich so ist - weil Sie ein nicht Func<string>in ein Action... konvertieren können und Sie keine Methodengruppe konvertieren können, die nur aus einer Methode besteht, die einen String in einen von Actionbeiden zurückgibt .
Jon Skeet
2
Sie können keinen Delegaten umwandeln, der keine Parameter hat und stringzu einem zurückkehrt Action. Ich verstehe nicht, warum es Mehrdeutigkeiten gibt.
Jason
3
@dtb: Ja, durch Entfernen der Überladung wird das Problem behoben - aber das erklärt nicht wirklich, warum es ein Problem gibt.
Jon Skeet