35 votes

Passer des fonctions lambda en tant que paramètres nommés en C #

Compilez ce programme simple:

class Program
{
    static void Foo( Action bar )
    {
        bar();
    }

    static void Main( string[] args )
    {
        Foo( () => Console.WriteLine( "42" ) );
    }
}

Rien d'étrange là. Si nous faisons une erreur dans la fonction lambda corps:

Foo( () => Console.LineWrite( "42" ) );

le compilateur renvoie un message d'erreur:

error CS0117: 'System.Console' does not contain a definition for 'LineWrite'

So far So good. Maintenant, nous allons utiliser un nom de paramètre dans l'appel à Foo:

Foo( bar: () => Console.LineWrite( "42" ) );

Cette fois, le compilateur les messages sont un peu confus:

error CS1502: The best overloaded method match for 
              'CA.Program.Foo(System.Action)' has some invalid arguments 
error CS1503: Argument 1: cannot convert from 'lambda expression' to 'System.Action'

Ce qui se passe? Pourquoi ne pas signaler les réels erreur?

Notez que nous ne obtenir le bon message d'erreur si nous utilisons une méthode anonyme au lieu de le lambda:

Foo( bar: delegate { Console.LineWrite( "42" ); } );

35voto

Eric Lippert Points 300275

Pourquoi ne pas signaler l'erreur?

Non, c'est le problème; c' est le signalement de l'erreur réelle.

Laissez-moi vous expliquer avec un exemple légèrement plus complexe. Supposons que vous avez ceci:

class CustomerCollection
{
    public IEnumerable<R> Select<R>(Func<Customer, R> projection) {...}
}
....
customers.Select( (Customer c)=>c.FristNmae );

OK, qu'est-ce que l'erreur en fonction de la spécification C# ? Vous devez lire la spécification de très prudemment. Nous allons travailler.

  • Nous avons un appel à Sélectionner comme un appel de fonction avec un argument simple et pas de type d'arguments. Nous faisons une recherche sur Sélectionner dans CustomerCollection, à la recherche de opposables choses nommé Sélectionnez-en fait, des choses comme des champs de type délégué, ou des méthodes. Puisque nous n'avons pas les arguments de type spécifié, nous faisons correspondre sur n'importe quel méthode générique Sélectionner. Nous trouvons l'un et construire une méthode de groupe hors de lui. La méthode de groupe ne contient qu'un seul élément.

  • La méthode de groupe doivent désormais être analysés par la résolution de surcharge de déterminer d'abord l' ensemble candidat, puis de déterminer l' applicables ensemble candidat, et de déterminer la meilleure candidate, et de déterminer l' a finalement validé la meilleure candidate. Si aucune de ces opérations ne sont pas ensuite de résolution de surcharge doit échouer avec une erreur. Dont l'un d'eux tombe en panne?

  • Nous commençons par la construction de l'ensemble candidat. Afin d'obtenir un candidat nous devons effectuer la méthode d'inférence de type pour déterminer la valeur de l'argument type R. Comment fonctionne la méthode d'inférence de type de travail?

  • Nous avons un lambda dont les types de paramètres sont tous connus -- le paramètre formel est Client. Afin de déterminer R, nous devons faire une correspondance entre le type de retour de la lambda à R. Quel est le type de retour de la lambda?

  • Nous supposons que c est à la Clientèle et de tenter d'analyser le corps de lambda. Faire une recherche de FristNmae dans le contexte de la Clientèle, et la recherche échoue.

  • Par conséquent, lambda retour de l'inférence de type échoue et aucune limite est ajouté à R.

  • Après tous les arguments sont analysés, il n'y a pas de limites sur R. la Méthode d'inférence de type est donc impossible de déterminer un type de R.

  • Par conséquent, la méthode d'inférence de type échoue.

  • Par conséquent, aucune méthode n'est ajouté à l'ensemble candidat.

  • Par conséquent, le candidat de l'ensemble est vide.

  • Par conséquent, il ne peut être applicable candidats.

  • Par conséquent, le corriger message d'erreur ici serait quelque chose comme "résolution de surcharge n'a pas pu trouver enfin validé la meilleure candidate, car le candidat était vide."

Les clients seraient très malheureux avec ce message d'erreur. Nous avons construit un grand nombre d'heuristiques dans le rapport d'erreur algorith qui tente de déterminer le plus "fondamentaux" de l'erreur que l'utilisateur puisse prendre des mesures concrètes pour corriger l'erreur. Nous avons raison:

  • L'erreur est que le candidat était vide. Pourquoi le candidat de l'ensemble vide?

  • Parce qu'il y a une méthode dans la méthode de groupe et de l'inférence de type a échoué.

OK, devrait nous rendre compte de l'erreur "résolution de surcharge a échoué parce que la méthode d'inférence de type a échoué"? Là encore, les clients serait malheureuse avec qui. Au lieu de cela, nous avons à nouveau poser la question "pourquoi avez-méthode d'inférence de type échec?"

  • Parce que l'ensemble relié de R était vide.

C'est une sale erreur de trop. Pourquoi les limites fixées vide?

  • Parce que le seul argument à partir de laquelle nous avons pu déterminer R est un lambda dont le type de retour ne peut pas être déduit.

OK, devrait nous rendre compte de l'erreur "résolution de surcharge a échoué car le lambda de retour de l'inférence de type a omis de déduire un type de retour"? De nouveau, les clients serait malheureuse avec qui. Au lieu de nous poser la question "pourquoi le lambda ne parviennent pas à en déduire un type de retour?"

  • Parce que le Client ne dispose pas d'un membre nommé FristNmae.

Et c' est l'erreur que nous en fait rapport.

Donc, vous voyez absolument tortueux de la chaîne de raisonnement que nous avons à parcourir afin de donner le message d'erreur que vous le souhaitez. Nous ne pouvons pas dire ce qui n'allait pas -- que la résolution de surcharge a été donné un vide candidat set -- nous devons creuser dans le passé pour déterminer comment la résolution de surcharge suis dans cet état.

Le code qui n'est donc extrêmement complexe; il traite avec des situations plus complexes que celle que j'ai présentée, y compris dans les cas où il y a n différentes méthodes génériques et l'inférence de type échoue pour les m raisons différentes et nous devons travailler à sortir de chez eux tous ce qu'est le "meilleur" de raison de donner à l'utilisateur. Rappelons que dans la réalité il y a une douzaine de différents types de Sélectionner et de résolution de surcharge sur chacun d'entre eux peut échouer pour des raisons différentes ou la même raison.

Il y a des heuristiques dans le rapport d'erreur du compilateur pour faire face à toutes sortes de surcharge de résolution des défaillances; celui que j'ai décrit, c'est juste l'un d'eux.

Alors maintenant, passons à votre cas particulier. Quelle est la véritable erreur?

  • Nous avons une méthode de groupe avec une seule méthode en elle, Foo. Peut-on construire un ensemble candidat?

  • Oui. Il y a un candidat. La méthode Foo est un candidat à l'appel, car elle a tout le nécessaire paramètre fourni-bar -- et pas de paramètres supplémentaires.

  • OK, le candidat a une méthode unique en elle. Est-il un membre de l'ensemble candidat?

  • Pas de. L'argument correspondant à la barre ne peut pas être convertie dans le type de paramètre formel parce que le corps de lambda contient une erreur.

  • Donc applicables ensemble candidat est vide, et il n'est donc pas validée définitivement la meilleure candidate, et par conséquent la résolution de surcharge échoue.

Alors, que devez-l'erreur de l'être? Encore une fois, nous ne pouvons pas simplement dire "résolution de surcharge pas réussi à trouver un validée définitivement la meilleure candidat" parce que les clients ne nous haïssent. Nous devons commencer à creuser pour le message d'erreur. Pourquoi la surcharge de la résolution de la panne?

  • Parce que l'applicables ensemble candidat était vide.

Pourquoi était-il vide?

  • Parce que chaque candidat dans elle a été rejetée.

Y avait-il un meilleur candidat possible?

  • Oui, il n'y a qu'un seul candidat.

Pourquoi était-il rejeté?

  • Parce que son argument n'a pas été converti vers le type de paramètre formel.

OK, à ce stade, apparemment, l'heuristique qui gère la surcharge de résolution de problèmes qui touchent les arguments nommés décide que nous avons creusé assez loin et que c'est l'erreur que nous devrions rapport. Si nous n'avons pas les arguments nommés à un autre heuristique demande:

Pourquoi l'argument non convertibles?

  • Parce que le corps de lambda, contenait une erreur.

Et puis, nous signaler cette erreur.

L'erreur heuristiques ne sont pas parfaits, loin de là. Par coïncidence, je suis cette semaine en faisant une lourde rearchitecture de la "simple" résolution de surcharge de rapport d'erreur heuristiques -- juste des trucs comme quand dire "il n'y avait pas une méthode qui a pris 2 paramètres" et quand à dire "la méthode que vous voulez, c'est privé" et dire "il n'y a pas de paramètre qui correspond à ce nom", et ainsi de suite; c'est tout à fait possible que vous appelez une méthode avec deux arguments, il n'existe pas de méthodes publiques de ce nom avec deux paramètres, il en est un qui est privé, mais l'un d'eux a un nom d'argument qui ne correspond pas. Rapide, quelle erreur devrions-nous le rapport? Nous avons deviner, et parfois, il y a une meilleure proposition que nous aurions faites, mais n'ont pas été suffisamment élaborée pour faire.

Même l'obtention de ce droit se révèle être une très délicat travail. Quand nous avons fini par arriver à recréer l'architecture de la grande lourds heuristiques -- comment faire face à l'échec de la méthode d'inférence de type à l'intérieur des expressions LINQ -- je vais revoir votre cas et voir si nous pouvons améliorer l'heuristique.

Mais depuis le message d'erreur que vous obtenez est complètement correct, ce n'est pas un bug du compilateur; plutôt, il s'agit d'une lacune du rapport d'erreur heuristique dans un cas particulier.

6voto

Josh E Points 4449

EDIT: Eric Lippert réponse décrit (beaucoup mieux) la question - s'il vous plaît voir sa réponse pour le "real deal"

MONTAGE FINAL: Comme peu flatteur que c'est pour l'une de laisser une démonstration publique de leur propre ignorance, dans la nature, il n'y a pas de gain de voile de l'ignorance derrière une pression sur le bouton supprimer. J'espère que quelqu'un d'autre peut bénéficier de mes rêveuse réponse :)

Merci Eric Lippert et svick d'être patient et de bien vouloir corriger ma compréhension erronée!


La raison que vous êtes l'obtention de la "mauvaise" message d'erreur ici c'est à cause de la variance et le compilateur de l'inférence de types combiné avec la façon dont le compilateur gère la résolution de type de paramètres nommés

Le type du premier exemple () => Console.LineWrite( "42" )

Grâce à la magie de l'inférence de type et de la covariance, ce qui donne le même résultat final que

Foo( bar: delegate { Console.LineWrite( "42" ); } );

Le premier bloc peut être soit de type LambdaExpression ou delegate; ce qui dépend de l'utilisation et de l'inférence.

Étant donné que, n'est-il pas étonnant que le compilateur devient confus quand vous passez un paramètre qui est censé être un Action mais qui pourrait être un covariant objet d'un type différent? Le message d'erreur est la clé principale qui pointe en direction de type résolution de la question.

Regardons les IL pour d'autres indices: Tous les exemples donnés compiler dans LINQPad:

IL_0000:  ldsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate1
IL_0005:  brtrue.s    IL_0018
IL_0007:  ldnull      
IL_0008:  ldftn       UserQuery.<Main>b__0
IL_000E:  newobj      System.Action..ctor
IL_0013:  stsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate1
IL_0018:  ldsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate1
IL_001D:  call        UserQuery.Foo

Foo:
IL_0000:  ldarg.0     
**IL_0001:  callvirt    System.Action.Invoke**
IL_0006:  ret         

<Main>b__0:
IL_0000:  ldstr       "42"
IL_0005:  call        System.Console.WriteLine
IL_000A:  ret

Remarque l' ** autour de l'appel à System.Action.Invoke: callvirt est exactement ce qu'il semble: une méthode virtuelle d'appel.

Lorsque vous appelez Foo avec un argument nommé, vous avez dit au compilateur que vous êtes en passant un Action, quand ce que vous êtes vraiment qui passe est un LambdaExpression. Normalement, c'est compilé (note de l' CachedAnonymousMethodDelegate1 dans le IL a appelé après le contractant pour l' Action) à un Action,, mais puisque vous le dit explicitement le compilateur vous avez été en passant une action, il tente d'utiliser l' LambdaExpression passé en Action, au lieu de la traiter comme une expression!

Bref: paramètre nommé résolution échoue en raison de l'erreur dans l'expression lambda (qui est un dur échec dans et de lui-même)

Voici les autres dire:

Action b = () => Console.LineWrite("42");
Foo(bar: b);

les rendements attendus message d'erreur.

Je ne suis probablement pas précis à 100% sur certains de le IL des trucs, mais j'espère que j'ai transmis l'idée générale

EDIT: dlev fait un grand point dans les commentaires de l'OP, sur l'ordre de la résolution de surcharge également jouer un rôle.

4voto

dlev Points 28160

Remarque: Pas vraiment une réponse, mais bien trop gros pour un commentaire.

Des résultats plus intéressants lorsque l'on jette dans l'inférence de type. Considérer ce code:

public class Test
{
    public static void Blah<T>(Action<T> blah)
    {
    }

    public static void Main()
    {
        Blah(x => { Console.LineWrite(x); });
    }
}

Il ne compile pas, car il n'y a pas de bonne façon de déterminer ce qu' T devraient l'être.
Message D'Erreur:

Le type des arguments pour une méthode 'Test.Blah<T>(System.Action<T>)'ne peut pas être déduite à partir de l'utilisation. Essayez de spécifier le type des arguments explicitement.

Du sens. Il faut préciser le type d' x explicitement et voir ce qui se passe:

public static void Main()
{
    Blah((int x) => { Console.LineWrite(x); });
}

Maintenant, les choses vont de travers parce qu' LineWrite n'existe pas.
Message D'Erreur:

'Système.Console' ne contient pas une définition pour 'LineWrite'

Également raisonnable. Maintenant, nous allons ajouter dans les arguments nommés et voir ce qui se passe. Tout d'abord, sans en préciser le type d' x:

public static void Main()
{
    Blah(blah: x => { Console.LineWrite(x); });
}

Nous nous attendons à obtenir un message d'erreur au sujet de ne pas être en mesure d'en déduire le type des arguments. Et nous le faisons. Mais ce n'est pas tout.
Les Messages D'Erreur:

Le type des arguments pour une méthode 'Test.Blah<T>(System.Action<T>)'ne peut pas être déduite à partir de l'utilisation. Essayez de spécifier le type des arguments explicitement.

'Système.Console' ne contient pas une définition pour 'LineWrite'

Neat. L'inférence de Type échoue, et nous nous sommes dit exactement pourquoi le lambda échec de la conversion. Ok, donc, nous allons spécifier le type d' x et voyons ce que nous obtenons:

public static void Main()
{
    Blah(blah: (int x) => { Console.LineWrite(x); });
}

Les Messages D'Erreur:

Le type des arguments pour une méthode 'Test.Blah<T>(System.Action<T>)'ne peut pas être déduite à partir de l'utilisation. Essayez de spécifier le type des arguments explicitement.

'Système.Console' ne contient pas une définition pour 'LineWrite'

Maintenant, c' est inattendu. L'inférence de Type est toujours pas (je suppose parce que le lambda -> Action<T> de conversion est de ne pas en, ainsi en niant le compilateur suppose que c' T est int) et rapports de la cause de l'échec.

TL; DR: je serai heureux quand Eric Lippert est à la recherche à l'heuristique pour ces cas plus complexes.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X