Errore di chiamata ambiguo del compilatore: metodo anonimo e gruppo di metodi con Func <> o Action


102

Ho uno scenario in cui desidero utilizzare la sintassi del gruppo di metodi anziché metodi anonimi (o sintassi lambda) per chiamare una funzione.

La funzione ha due overload, uno che prende un Action, l'altro prende a Func<string>.

Posso tranquillamente chiamare i due overload utilizzando metodi anonimi (o sintassi lambda), ma ottengo un errore del compilatore di invocazione ambigua se utilizzo la sintassi del gruppo di metodi. Posso aggirare il problema eseguendo il casting esplicito su Actiono Func<string>, ma non credo che dovrebbe essere necessario.

Qualcuno può spiegare perché dovrebbero essere richiesti i cast espliciti.

Esempio di codice di seguito.

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

Aggiornamento C # 7.3

Come da commento di 0xcde di seguito del 20 marzo 2019 (nove anni dopo aver pubblicato questa domanda!), Questo codice viene compilato a partire da C # 7.3 grazie a candidati al sovraccarico migliorati .


Ho provato il tuo codice e ricevo un ulteriore errore in fase di compilazione: 'void test.ClassWithSimpleMethods.DoNothing ()' ha il tipo di ritorno sbagliato (che è sulla riga 25, che è dove si trova l'errore di ambiguità)
Matt Ellen

@ Matt: vedo anche quell'errore. Gli errori che ho citato nel mio post erano i problemi di compilazione che VS evidenzia prima ancora di provare una compilazione completa.
Richard Ev

1
A proposito, questa era un'ottima domanda. Amo tutto ciò che mi costringe a rispettare le specifiche :)
Jon Skeet

1
Si noti che il codice di esempio verrà compilato se si usa C # 7.3 ( <LangVersion>7.3</LangVersion>) o versioni successive grazie a candidati all'overload migliorati .
0xced il

Risposte:


97

Prima di tutto, lasciatemi dire che la risposta di Jon è corretta. Questa è una delle parti più pelose della specifica, così bravo con Jon per essersi tuffato per primo.

In secondo luogo, lasciatemi dire che questa riga:

Esiste una conversione implicita da un gruppo di metodi a un tipo di delegato compatibile

(enfasi aggiunta) è profondamente fuorviante e sfortunato. Parlerò con Mads sulla rimozione della parola "compatibile" qui.

Il motivo per cui questo è fuorviante e sfortunato è perché sembra che questo richiami alla sezione 15.2, "Delegare la compatibilità". La sezione 15.2 ha descritto la relazione di compatibilità tra metodi e tipi di delegato , ma questa è una questione di convertibilità di gruppi di metodi e tipi di delegati , che è diversa.

Ora che l'abbiamo tolto di mezzo, possiamo esaminare la sezione 6.6 delle specifiche e vedere cosa otteniamo.

Per eseguire la risoluzione del sovraccarico, è necessario prima determinare quali sovraccarichi sono candidati applicabili . Un candidato è applicabile se tutti gli argomenti sono convertibili in modo implicito nei tipi di parametri formali. Considera questa versione semplificata del tuo programma:

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

Quindi esaminiamolo riga per riga.

Esiste una conversione implicita da un gruppo di metodi a un tipo di delegato compatibile.

Ho già discusso di come la parola "compatibile" sia sfortunata qui. Andare avanti. Ci stiamo chiedendo quando si esegue la risoluzione del sovraccarico su Y (X), il gruppo di metodi X viene convertito in D1? Si converte in D2?

Dato un tipo di delegato D e un'espressione E classificata come gruppo di metodi, esiste una conversione implicita da E a D se E contiene almeno un metodo applicabile [...] a un elenco di argomenti costruito utilizzando il parametro tipi e modificatori di D, come descritto di seguito.

Fin qui tutto bene. X potrebbe contenere un metodo applicabile con gli elenchi di argomenti di D1 o D2.

L'applicazione in fase di compilazione di una conversione da un gruppo di metodi E a un tipo delegato D è descritta di seguito.

Questa riga non dice davvero nulla di interessante.

Si noti che l'esistenza di una conversione implicita da E a D non garantisce che l'applicazione in fase di compilazione della conversione avrà esito positivo senza errori.

Questa linea è affascinante. Significa che esistono conversioni implicite, ma che sono soggette a trasformarsi in errori! Questa è una regola bizzarra di C #. Per divagare un momento, ecco un esempio:

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

Un'operazione di incremento è illegale in un albero delle espressioni. Tuttavia, il lambda è ancora convertibile nel tipo di albero delle espressioni, anche se la conversione viene mai utilizzata, è un errore! Il principio qui è che potremmo voler cambiare le regole di ciò che può andare in un albero delle espressioni in seguito; la modifica di tali regole non dovrebbe modificare le regole del sistema di tipo . Vogliamo costringerti a rendere i tuoi programmi inequivocabili ora , in modo che quando in futuro modificheremo le regole per gli alberi delle espressioni per migliorarli, non introdurremo modifiche sostanziali nella risoluzione del sovraccarico .

Comunque, questo è un altro esempio di questa sorta di regola bizzarra. Può esistere una conversione ai fini della risoluzione del sovraccarico, ma essere un errore da utilizzare effettivamente. Anche se in realtà non è esattamente la situazione in cui ci troviamo qui.

Andare avanti:

Viene selezionato un singolo metodo M corrispondente a una invocazione di metodo della forma E (A) [...] La lista di argomenti A è una lista di espressioni, ciascuna classificata come variabile [...] del parametro corrispondente nel modulo formale -parameter-list di D.

OK. Quindi eseguiamo la risoluzione del sovraccarico su X rispetto a D1. L'elenco dei parametri formali di D1 è vuoto, quindi eseguiamo la risoluzione dell'overload su X () e joy, troviamo un metodo "string X ()" che funziona. Allo stesso modo, l'elenco dei parametri formali di D2 è vuoto. Ancora una volta, troviamo che "string X ()" è un metodo che funziona anche qui.

Il principio qui è che la determinazione della convertibilità del gruppo di metodi richiede la selezione di un metodo da un gruppo di metodi usando la risoluzione dell'overload e la risoluzione dell'overload non considera i tipi restituiti .

Se l'algoritmo [...] produce un errore, si verifica un errore in fase di compilazione. Altrimenti l'algoritmo produce un unico metodo migliore M avente lo stesso numero di parametri di D e la conversione è considerata esistente.

C'è solo un metodo nel gruppo di metodi X, quindi deve essere il migliore. Abbiamo dimostrato con successo che esiste una conversione da X a D1 e da X a D2.

Ora, questa linea è pertinente?

Il metodo selezionato M deve essere compatibile con il tipo di delegato D, altrimenti si verifica un errore in fase di compilazione.

In realtà no, non in questo programma. Non arriviamo mai all'attivazione di questa linea. Perché, ricorda, quello che stiamo facendo qui è cercare di eseguire la risoluzione del sovraccarico su Y (X). Abbiamo due candidati Y (D1) e Y (D2). Entrambi sono applicabili. Quale è meglio ? In nessun punto delle specifiche descriviamo la migliore tra queste due possibili conversioni .

Ora, si potrebbe certamente sostenere che una conversione valida è migliore di una che produce un errore. Ciò significherebbe quindi effettivamente, in questo caso, che la risoluzione del sovraccarico prende in considerazione i tipi restituiti, che è qualcosa che vogliamo evitare. La domanda quindi è quale principio sia migliore: (1) mantenere l'invariante che la risoluzione del sovraccarico non considera i tipi restituiti, o (2) provare a scegliere una conversione che sappiamo funzionerà su una che sappiamo non lo farà?

Questa è una chiamata di giudizio. Con lambdas , lo facciamo prendere in considerazione il tipo di ritorno in questi tipi di conversioni, nella sezione 7.4.3.3:

E è una funzione anonima, T1 e T2 sono tipi delegati o tipi di albero delle espressioni con elenchi di parametri identici, esiste un tipo di ritorno dedotto X per E nel contesto di tale elenco di parametri e vale uno dei seguenti:

  • T1 ha un tipo di ritorno Y1 e T2 ha un tipo di ritorno Y2 e la conversione da X a Y1 è migliore della conversione da X a Y2

  • T1 ha un tipo restituito Y e T2 non restituisce nulla

È un peccato che le conversioni del gruppo di metodi e le conversioni lambda siano incoerenti sotto questo aspetto. Tuttavia, posso conviverci.

Ad ogni modo, non abbiamo una regola "migliore" per determinare quale conversione è migliore, X in D1 o X in D2. Pertanto diamo un errore di ambiguità sulla risoluzione di Y (X).


8
Cracking: molte grazie sia per la risposta che (si spera) per il miglioramento risultante nelle specifiche :) Personalmente penso che sarebbe ragionevole per la risoluzione del sovraccarico prendere in considerazione il tipo restituito per le conversioni del gruppo di metodi al fine di rendere il comportamento più intuitivo, ma Capisco che lo farebbe a scapito della coerenza. (Lo stesso si può dire dell'inferenza di tipo generico applicata alle conversioni del gruppo di metodi quando c'è un solo metodo nel gruppo di metodi, come penso di aver discusso in precedenza.)
Jon Skeet

35

EDIT: Penso di averlo capito.

Come dice zinglon, è perché c'è una conversione implicita da GetStringa Actionanche se l'applicazione in fase di compilazione fallirebbe. Ecco l'introduzione alla sezione 6.6, con qualche enfasi (mia):

Esiste una conversione implicita (§6.1) da un gruppo di metodi (§7.1) a un tipo di delegato compatibile. Dato un tipo di delegato D e un'espressione E classificata come gruppo di metodi, esiste una conversione implicita da E a D se E contiene almeno un metodo che è applicabile nella sua forma normale (§7.4.3.1) a un elenco di argomenti costruito mediante l'uso dei tipi di parametro e dei modificatori di D , come descritto di seguito.

Ora, ero confuso dalla prima frase, che parla di una conversione in un tipo di delegato compatibile. Actionnon è un delegato compatibile per alcun metodo nel GetStringgruppo di metodi, ma il GetString()metodo è applicabile nella sua forma normale a un elenco di argomenti costruito utilizzando i tipi di parametro e i modificatori di D. Si noti che questo non parla del tipo restituito di D. Ecco perché si confonde ... perché controllerebbe solo la compatibilità dei delegati GetString()quando applica la conversione, non la sua esistenza.

Penso che sia istruttivo lasciare brevemente fuori dall'equazione il sovraccarico e vedere come può manifestarsi questa differenza tra l' esistenza di una conversione e la sua applicabilità . Ecco un esempio breve ma completo:

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

Nessuna delle espressioni di Mainchiamata del metodo nelle compilazioni, ma i messaggi di errore sono diversi. Ecco quello per IntMethod(GetString):

Test.cs (12,9): errore CS1502: la migliore corrispondenza del metodo sovraccarico per "Program.IntMethod (int)" ha alcuni argomenti non validi

In altre parole, la sezione 7.4.3.1 delle specifiche non riesce a trovare alcun membro di funzione applicabile.

Ora ecco l'errore per ActionMethod(GetString):

Test.cs (13,22): errore CS0407: "stringa Program.GetString ()" ha il tipo restituito errato

Questa volta ha elaborato il metodo che desidera chiamare, ma non è riuscito a eseguire la conversione richiesta. Sfortunatamente non riesco a trovare la parte delle specifiche in cui viene eseguito il controllo finale - sembra che potrebbe essere in 7.5.5.1, ma non riesco a vedere esattamente dove.


Vecchia risposta rimossa, ad eccezione di questa parte - perché mi aspetto che Eric possa far luce sul "perché" di questa domanda ...

Stai ancora cercando ... nel frattempo, se diciamo "Eric Lippert" tre volte, pensi che avremo una visita (e quindi una risposta)?


@ Jon - potrebbe essere quello classWithSimpleMethods.GetStringe classWithSimpleMethods.DoNothingnon sono delegati?
Daniel A. White

@Daniel: No, quelle espressioni sono espressioni del gruppo di metodi e i metodi sovraccaricati dovrebbero essere considerati applicabili solo quando c'è una conversione implicita dal gruppo di metodi al tipo di parametro pertinente. Vedere la sezione 7.4.3.1 delle specifiche.
Jon Skeet

Leggendo la sezione 6.6, sembra che la conversione da classWithSimpleMethods.GetString ad Action sia considerata esistente poiché gli elenchi di parametri sono compatibili, ma che la conversione (se tentata) fallisca in fase di compilazione. Pertanto, una conversione implicita non esistono per entrambi i tipi delegato e la chiamata è ambigua.
zinglon

@zinglon: come stai leggendo §6.6 per determinare che una conversione da ClassWithSimpleMethods.GetStringa Actionè valida? Affinché un metodo Msia compatibile con un tipo delegatoD (§15.2) "esiste un'identità o una conversione di riferimento implicita dal tipo restituito di Mal tipo restituito di D."
jason

@ Jason: Le specifiche non dicono che la conversione è valida, lo dice esiste . In effetti, non è valido poiché non riesce in fase di compilazione. I primi due punti del §6.6 determinano se la conversione esiste. I seguenti punti determinano se la conversione avrà successo. Dal punto 2: "Altrimenti l'algoritmo produce un unico metodo migliore M avente lo stesso numero di parametri di D e la conversione è considerata esistente". §15.2 è invocato al punto 3.
zinglon

1

utilizzando Func<string> and Action<string>(ovviamente molto diverso da Actione Func<string>) nel ClassWithDelegateMethodsrimuove l'ambiguità.

L'ambiguità si verifica anche tra Actione Func<int>.

Ottengo anche l'errore di ambiguità con questo:

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

Ulteriori sperimentazioni mostrano che quando si passa un gruppo di metodi da solo, il tipo restituito viene completamente ignorato quando si determina quale sovraccarico usare.

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

Il sovraccarico con Funce Actionè simile (perché entrambi sono delegati) a

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

void Function() // Action
{
}

Se noti, il compilatore non sa quale chiamare perché differiscono solo per i tipi restituiti.


Non penso che sia proprio così - perché non puoi convertire a Func<string>in un Action... e non puoi convertire un gruppo di metodi costituito solo da un metodo che restituisce una stringa in un Action.
Jon Skeet

2
Non è possibile eseguire il cast di un delegato che non ha parametri e ritorna stringa un file Action. Non vedo perché ci sia ambiguità.
jason

3
@dtb: Sì, la rimozione del sovraccarico rimuove il problema, ma questo non spiega davvero perché c'è un problema.
Jon Skeet
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.