Erreur d'appel ambiguë du compilateur - méthode anonyme et groupe de méthodes avec Func <> ou Action

102

J'ai un scénario dans lequel je souhaite utiliser la syntaxe du groupe de méthodes plutôt que des méthodes anonymes (ou la syntaxe lambda) pour appeler une fonction.

La fonction a deux surcharges, l'une qui prend un Action, l'autre prend un Func<string>.

Je peux heureusement appeler les deux surcharges en utilisant des méthodes anonymes (ou la syntaxe lambda), mais obtenir une erreur du compilateur d' invocation ambiguë si j'utilise la syntaxe de groupe de méthodes. Je peux contourner le problème par un casting explicite vers Actionou Func<string>, mais je ne pense pas que cela devrait être nécessaire.

Quelqu'un peut-il expliquer pourquoi les moulages explicites devraient être requis.

Exemple de code ci-dessous.

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

Mise à jour C # 7.3

Selon le commentaire de 0xcde ci-dessous le 20 mars 2019 (neuf ans après avoir posté cette question!), Ce code se compile à partir de C # 7.3 grâce à des candidats de surcharge améliorés .

Richard Ev
la source
J'ai essayé votre code et j'obtiens une erreur de compilation supplémentaire: 'void test.ClassWithSimpleMethods.DoNothing ()' a le mauvais type de retour (qui est à la ligne 25, où se trouve l'erreur d'ambiguïté)
Matt Ellen
@Matt: Je vois aussi cette erreur. Les erreurs que j'ai citées dans mon article étaient les problèmes de compilation que VS met en évidence avant même d'essayer une compilation complète.
Richard Ev
1
Au fait, c'était une excellente question. J'aime tout ce qui me force à entrer dans les spécifications :)
Jon Skeet
1
Notez que votre exemple de code sera compilé si vous utilisez C # 7.3 ( <LangVersion>7.3</LangVersion>) ou version ultérieure grâce à des candidats de surcharge améliorés .
0xced

Réponses:

97

Tout d'abord, permettez-moi de dire que la réponse de Jon est correcte. C'est l'une des parties les plus poilues de la spécification, donc bon Jon pour y plonger la tête la première.

Deuxièmement, laissez-moi dire que cette ligne:

Une conversion implicite existe d'un groupe de méthodes vers un type de délégué compatible

(je souligne) est profondément trompeur et malheureux. Je vais avoir une discussion avec Mads sur la suppression du mot «compatible» ici.

La raison pour laquelle cela est trompeur et malheureux est qu'il semble que cela appelle à la section 15.2, «Compatibilité des délégués». La section 15.2 décrit la relation de compatibilité entre les méthodes et les types de délégués , mais il s'agit d'une question de convertibilité des groupes de méthodes et des types de délégués , ce qui est différent.

Maintenant que nous avons éliminé cela, nous pouvons parcourir la section 6.6 de la spécification et voir ce que nous obtenons.

Pour résoudre les surcharges, nous devons d'abord déterminer quelles surcharges sont des candidats applicables . Un candidat est applicable si tous les arguments sont implicitement convertibles en types de paramètres formels. Considérez cette version simplifiée de votre programme:

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

Alors passons en revue ligne par ligne.

Une conversion implicite existe à partir d'un groupe de méthodes vers un type de délégué compatible.

J'ai déjà expliqué comment le mot "compatible" est malheureux ici. Passer à autre chose. Nous nous demandons lors de la résolution de surcharge sur Y (X), le groupe de méthodes X se convertit-il en D1? Se transforme-t-il en D2?

Étant donné un délégué de type D et une expression E classée comme groupe de méthodes, une conversion implicite existe de E en D si E contient au moins une méthode applicable [...] à une liste d'arguments construite à l'aide du paramètre types et modificateurs de D, comme décrit ci-dessous.

Jusqu'ici tout va bien. X peut contenir une méthode applicable avec les listes d'arguments de D1 ou D2.

L'application au moment de la compilation d'une conversion d'un groupe de méthodes E en un délégué de type D est décrite ci-dessous.

Cette ligne ne dit vraiment rien d'intéressant.

Notez que l'existence d'une conversion implicite de E en D ne garantit pas que l'application à la compilation de la conversion réussira sans erreur.

Cette ligne est fascinante. Cela signifie qu'il y a des conversions implicites qui existent, mais qui sont susceptibles d'être transformées en erreurs! C'est une règle bizarre de C #. Pour faire une digression un instant, voici un exemple:

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

Une opération d'incrémentation est illégale dans une arborescence d'expression. Cependant, le lambda est toujours convertible en type d'arborescence d'expression, même si si la conversion est déjà utilisée, c'est une erreur! Le principe ici est que nous pourrions vouloir changer les règles de ce qui peut aller dans un arbre d'expression plus tard; la modification de ces règles ne doit pas modifier les règles du système de types . Nous voulons vous obliger à rendre vos programmes sans ambiguïté maintenant , de sorte que lorsque nous changerons les règles des arbres d'expression à l'avenir pour les améliorer, nous n'introduisons pas de changements de rupture dans la résolution des surcharges .

Quoi qu'il en soit, c'est un autre exemple de ce genre de règle bizarre. Une conversion peut exister à des fins de résolution de surcharge, mais il s'agit d'une erreur à utiliser réellement. Bien qu'en fait, ce ne soit pas exactement la situation dans laquelle nous nous trouvons ici.

Passer à autre chose:

Une seule méthode M est sélectionnée correspondant à une invocation de méthode de la forme E (A) [...] La liste d'arguments A est une liste d'expressions, chacune classée comme une variable [...] du paramètre correspondant au format formel -liste-des-paramètres de D.

D'ACCORD. Nous faisons donc une résolution de surcharge sur X par rapport à D1. La liste des paramètres formels de D1 est vide, donc nous faisons une résolution de surcharge sur X () et joy, nous trouvons une méthode "string X ()" qui fonctionne. De même, la liste des paramètres formels de D2 est vide. Encore une fois, nous trouvons que "string X ()" est une méthode qui fonctionne ici aussi.

Le principe ici est que la détermination de la convertibilité d'un groupe de méthodes nécessite la sélection d'une méthode dans un groupe de méthodes utilisant la résolution de surcharge , et la résolution de surcharge ne prend pas en compte les types de retour .

Si l'algorithme [...] produit une erreur, une erreur de compilation se produit. Sinon, l'algorithme produit une seule meilleure méthode M ayant le même nombre de paramètres que D et la conversion est considérée comme existante.

Il n'y a qu'une seule méthode dans le groupe de méthodes X, elle doit donc être la meilleure. Nous avons prouvé avec succès qu'il existe une conversion de X vers D1 et de X vers D2.

Maintenant, cette ligne est-elle pertinente?

La méthode sélectionnée M doit être compatible avec le type de délégué D, sinon, une erreur de compilation se produit.

En fait, non, pas dans ce programme. Nous n'allons jamais jusqu'à activer cette ligne. Parce que, rappelez-vous, ce que nous faisons ici, c'est essayer de faire une résolution de surcharge sur Y (X). Nous avons deux candidats Y (D1) et Y (D2). Les deux sont applicables. Quel est le meilleur ? Nulle part dans la spécification nous ne décrivons l'amertume entre ces deux conversions possibles .

Maintenant, on pourrait certainement affirmer qu'une conversion valide est meilleure qu'une conversion qui produit une erreur. Cela signifierait alors effectivement, dans ce cas, que la résolution de surcharge prend en compte les types de retour, ce que nous voulons éviter. La question est alors de savoir quel principe est le meilleur: (1) maintenir l'invariant selon lequel la résolution de surcharge ne prend pas en compte les types de retour, ou (2) essayer de choisir une conversion dont nous savons qu'elle fonctionnera sur une conversion que nous savons qu'elle ne fonctionnera pas?

C'est un appel au jugement. Avec lambdas , nous faisons considérer le type de retour dans ce genre de conversions, dans la section 7.4.3.3:

E est une fonction anonyme, T1 et T2 sont des types délégués ou des types d'arbres d'expression avec des listes de paramètres identiques, un type de retour déduit X existe pour E dans le contexte de cette liste de paramètres, et l'un des éléments suivants est maintenu:

  • T1 a un type de retour Y1 et T2 a un type de retour Y2, et la conversion de X en Y1 est meilleure que la conversion de X en Y2

  • T1 a un type de retour Y et T2 est nul retour

Il est regrettable que les conversions de groupes de méthodes et les conversions lambda soient incohérentes à cet égard. Cependant, je peux vivre avec.

Quoi qu'il en soit, nous n'avons pas de règle «d'amertume» pour déterminer quelle conversion est la meilleure, X en D1 ou X en D2. Nous donnons donc une erreur d'ambiguïté sur la résolution de Y (X).

Eric Lippert
la source
8
Cracking - merci beaucoup pour la réponse et (espérons-le) pour l'amélioration de la spécification qui en résulte :) Personnellement, je pense qu'il serait raisonnable que la résolution de surcharge prenne en compte le type de retour pour les conversions de groupes de méthodes afin de rendre le comportement plus intuitif, mais Je comprends que cela se ferait au détriment de la cohérence. (On peut en dire autant de l'inférence de type générique appliquée aux conversions de groupe de méthodes lorsqu'il n'y a qu'une seule méthode dans le groupe de méthodes, comme je pense que nous l'avons déjà discuté.)
Jon Skeet
35

EDIT: Je pense que je l'ai.

Comme le dit zinglon, c'est parce qu'il y a une conversion implicite de GetStringen Actionmême si l'application au moment de la compilation échouerait. Voici l'introduction à la section 6.6, avec un peu d'emphase (la mienne):

Une conversion implicite (§6.1) existe d'un groupe de méthodes (§7.1) à un type délégué compatible. Étant donné un délégué de type D et une expression E classée comme groupe de méthodes, il existe une conversion implicite de E en D si E contient au moins une méthode applicable sous sa forme normale (§7.4.3.1) à une liste d'arguments construite en utilisant les types de paramètres et les modificateurs de D , comme décrit ci-dessous.

Maintenant, j'étais confus par la première phrase - qui parle d'une conversion en un type de délégué compatible. Actionn'est pas un délégué compatible pour aucune méthode du GetStringgroupe de méthodes, mais la GetString()méthode est applicable sous sa forme normale à une liste d'arguments construite à l'aide des types de paramètres et des modificateurs de D. Notez que ce n'est pas le cas parle du type de retour de D. C'est pourquoi cela devient confus ... parce qu'il vérifierait uniquement la compatibilité des délégués GetString()lors de l' application de la conversion, sans vérifier son existence.

Je pense qu'il est instructif de laisser brièvement la surcharge hors de l'équation et de voir comment cette différence entre l' existence d' une conversion et son applicabilité peut se manifester. Voici un exemple court mais complet:

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

Aucune des expressions d'appel de méthode dans les Maincompilations, mais les messages d'erreur sont différents. Voici celui pour IntMethod(GetString):

Test.cs (12,9): erreur CS1502: la meilleure correspondance de méthode surchargée pour 'Program.IntMethod (int)' a des arguments non valides

En d'autres termes, la section 7.4.3.1 de la spécification ne trouve aucun membre de fonction applicable.

Voici maintenant l'erreur pour ActionMethod(GetString):

Test.cs (13,22): erreur CS0407: 'string Program.GetString ()' a le mauvais type de retour

Cette fois, il a déterminé la méthode qu'il souhaite appeler - mais il n'a pas réussi à effectuer la conversion requise. Malheureusement, je ne peux pas trouver le détail de la spécification où cette vérification finale est effectuée - il semble que cela pourrait être dans 7.5.5.1, mais je ne peux pas voir exactement où.


Ancienne réponse supprimée, à l'exception de ce bit - car je pense qu'Eric pourrait éclairer le "pourquoi" de cette question ...

Toujours à la recherche ... en attendant, si on dit "Eric Lippert" trois fois, pensez-vous que nous aurons une visite (et donc une réponse)?

Jon Skeet
la source
@Jon - pourrait-il être cela classWithSimpleMethods.GetStringet classWithSimpleMethods.DoNothingne sont pas des délégués?
Daniel A. White
@Daniel: Non - ces expressions sont des expressions de groupe de méthodes, et les méthodes surchargées ne devraient être considérées comme applicables qu'en cas de conversion implicite du groupe de méthodes vers le type de paramètre approprié. Voir la section 7.4.3.1 de la spécification.
Jon Skeet
En lisant la section 6.6, il semble que la conversion de classWithSimpleMethods.GetString en Action est considérée comme existante puisque les listes de paramètres sont compatibles, mais que la conversion (si elle est tentée) échoue au moment de la compilation. Par conséquent, une conversion implicite n'existe pour les deux types de délégués et l'appel est ambigu.
zinglon
@zinglon: Comment lisez-vous le §6.6 pour déterminer qu'une conversion de ClassWithSimpleMethods.GetStringà Actionest valide? Pour qu'une méthode Msoit compatible avec un type délégué D(§15.2) "une identité ou une conversion de référence implicite existe du type de Mretour de au type de retour de D."
jason
@Jason: La spécification ne dit pas que la conversion est valide, elle dit qu'elle existe . En fait, il est invalide car il échoue au moment de la compilation. Les deux premiers points du §6.6 déterminent si la conversion existe. Les points suivants déterminent si la conversion réussira. Du point 2: "Sinon, l'algorithme produit une seule meilleure méthode M ayant le même nombre de paramètres que D et la conversion est considérée comme existante." §15.2 est invoqué au point 3.
Zinglon
1

Utiliser Func<string>et Action<string>(évidemment très différent de Actionet Func<string>) dans leClassWithDelegateMethods supprime l'ambiguïté.

L'ambiguïté se produit également entre ActionetFunc<int> .

J'obtiens également l'erreur d'ambiguïté avec ceci:

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

Une expérimentation plus poussée montre que lors du passage d'un groupe de méthodes par lui-même, le type de retour est complètement ignoré lors de la détermination de la surcharge à utiliser.

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
la source
0

La surcharge avec Funcet Actions'apparente (car les deux sont des délégués) à

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

void Function() // Action
{
}

Si vous remarquez, le compilateur ne sait pas lequel appeler car ils ne diffèrent que par les types de retour.

Daniel A. White
la source
Je ne pense pas que ce soit vraiment comme ça - parce que vous ne pouvez pas convertir un Func<string>en un Action... et vous ne pouvez pas convertir un groupe de méthodes composé uniquement d'une méthode qui renvoie une chaîne en un Actionnon plus.
Jon Skeet
2
Vous ne pouvez pas transtyper un délégué qui n'a pas de paramètres et retourne stringà un Action. Je ne vois pas pourquoi il y a ambiguïté.
jason
3
@dtb: Oui, la suppression de la surcharge supprime le problème - mais cela n'explique pas vraiment pourquoi il y a un problème.
Jon Skeet