J'ai un scénario où je veux utiliser la syntaxe de groupe de méthodes plutôt que des méthodes anonymes (ou 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 j'obtiens une erreur de compilation de Invocation ambiguë si j'utilise la syntaxe du groupe de méthodes. Je peux contourner ce problème en effectuant un cast explicite vers Action
ou Func<string>
, mais ne pense pas que cela devrait être nécessaire.
Quelqu'un peut-il expliquer pourquoi les transtypages 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() { }
}
Selon le commentaire de xcde 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 à candidats à la surcharge améliorée .
Tout d'abord, permettez-moi de dire que la réponse de Jon est correcte. C'est l'une des parties les plus velues de la spécification, donc bon pour Jon pour plonger dedans la tête la première.
Deuxièmement, permettez-moi de dire que cette ligne:
Une conversion implicite existe d'un groupe de méthodes vers un type délégué compatible
(je souligne) est profondément trompeur et regrettable. J'aurai une discussion avec Mads sur la suppression du mot "compatible" ici.
La raison pour laquelle cela est trompeur et regrettable 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 méthodes et types délégués, mais c'est une question de convertibilité de groupes de méthodes et types 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);
}
}
Passons donc 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é à quel point le mot "compatible" est malheureux ici. Continuons. Nous nous demandons lors de la résolution de surcharge sur Y (X), le groupe de méthodes X se convertit-il en D1? Convertit-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 vers 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 aux listes d'arguments D1 ou D2.
L'application au moment de la compilation d'une conversion d'un groupe de méthodes E en un type délégué 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 au moment de 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 peuvent être transformées en erreurs! C'est une règle bizarre de C #. Pour m'éloigner 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'expressions. Cependant, le lambda est toujours convertible en type d'arbre d'expression, même si jamais la conversion est 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 système de type . Nous voulons vous forcer à 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 de surcharge .
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 être une erreur à utiliser réellement. En fait, ce n'est pas exactement la situation dans laquelle nous nous trouvons ici.
Passons à 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 variable [.. .] du paramètre correspondant dans la liste des paramètres formels 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 joie, 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 constatons que "string X ()" est une méthode qui fonctionne ici aussi.
Le principe ici est que pour déterminer la convertibilité d'un groupe de méthodes, il faut sélectionner une méthode dans un groupe de méthodes en utilisant la résolution de surcharge , et la résolution de surcharge ne prend pas en compte le retour types.
Si l'algorithme [...] produit une erreur, une erreur de compilation se produit. Sinon, l'algorithme produit une meilleure méthode M ayant le même nombre de paramètres que D et la conversion est considérée comme existant.
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'une conversion existe de X à D1 et de X à D2.
Maintenant, cette ligne est-elle pertinente?
La méthode sélectionnée M doit être compatible avec le type 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 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 que celle qui produit une erreur. Cela reviendrait alors à dire, 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 par rapport à celle que nous savons ne pas?
Ceci est un appel au jugement. Avec lambdas , nous considérons le type de retour dans ces sortes 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'arbre 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'une des conditions suivantes est remplie:
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 un retour nul
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 de "bêtise" 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).
EDIT: Je pense que je l'ai.
Comme le dit zinglon, c'est parce qu'il y a une conversion implicite de GetString
vers Action
même si l'application au moment de la compilation échoue. Voici l'introduction de la section 6.6, avec un peu d'emphase (la mienne):
Une conversion implicite (§6.1) existe depuis un groupe de méthodes (§7.1) vers 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, une conversion implicite existe de E vers D si E contient au moins une méthode qui est 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, je devenais confus par la première phrase - qui parle d'une conversion en un type délégué compatible. Action
n'est un délégué compatible pour aucune méthode du groupe de méthodes GetString
, mais la méthode GetString()
est applicable sous sa forme normale à une liste d'arguments construite en utilisant les types de paramètres et les modificateurs de D. Notez que ce ne parle pas à propos du type de retour de D. C'est pourquoi cela devient confus ... car il ne vérifierait la compatibilité des délégués de GetString()
que lorsque appliquer la conversion, sans vérifier son existence.
Je pense qu'il est instructif de laisser la surcharge hors de l'équation brièvement et de voir comment cette différence entre l'existence d'une conversion et sa l'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 Main
ne se compile, 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)" contient 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éfini 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 bout de la spécification où cette vérification finale est effectuée - il semble que pourrait être en 7.5.5.1, mais je ne peut pas voir exactement où.
Ancienne réponse supprimée, sauf pour ce bit - parce que je m'attends à ce qu'Eric puisse é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)?
En utilisant Func<string>
et Action<string>
(évidemment très différent de Action
et Func<string>
) dans le ClassWithDelegateMethods
supprime l'ambiguïté.
L'ambiguïté se produit également entre Action
et Func<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 dans 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(); }
}
La surcharge avec Func
et Action
s'apparente (car les deux sont 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.