1
votes

Comment le compilateur sait-il quel type de données doit être l'expression lambda

Ainsi, lorsque vous utilisez EF Core et que vous utilisez la plupart des extensions Linq, vous utilisez en fait System.Linq.Expressions au lieu de l'habituel Func .

Alors disons vous utilisez FirstOrDefault sur un DbSet.

Expression<Func<Entity, bool>> = x => x.Bar == true;

Lorsque vous ctrl + lmb sur FirstOrDefault il vous montrera la surcharge suivante:

Func<Entity, bool> = x => x.Bar == true;

Mais il y a aussi une surcharge pour Func :

public static TSource FirstOrDefault<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)

Lorsque vous souhaitez stocker une expression dans une variable, vous pouvez faire quelque chose comme ceci:

public static TSource FirstOrDefault<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate)

et

DbContext.Foos.FirstOrDefault(x=> x.Bar == true);

Alors, comment le compilateur décide-t-il quelle surcharge doit être utilisée lors de l'utilisation de ces méthodes d'extension?


5 commentaires

C'est compliqué ... docs.microsoft .com / fr-fr / dotnet / visual-basic / reference /…


Dans le cas d'une func comme paramètre d'entrée pour FirstAndDefault (), le compilateur peut uniquement utiliser la surcharge IEnumerable car une func <> ne peut pas être traduite en SQL pour être transmise à la base de données. Cela pourrait avoir quelque chose à voir avec cela.


@DavidG Je suppose que cela s'applique à .Net en général?


@Bensjero Eh bien, cela n'a toujours pas de sens.


Je me rends compte que j'ai lié aux documents VB au lieu de C #. C # a ses propres règles.


3 Réponses :


0
votes

Je pense que l'endroit le plus utile pour consulter la spécification C # est Expressions de fonction anonymes :

Une fonction anonyme n'a pas de valeur ou de type en soi, mais est convertible en un délégué ou un type d'arborescence d'expression compatible

...

Dans une liste de paramètres typée implicitement, les types des paramètres sont déduits du contexte dans lequel la fonction anonyme se produit - en particulier, lorsque la fonction anonyme est convertie en un type de délégué compatible ou un type d'arborescence d'expression, ce type fournit le paramètre types.

Ce qui nous conduit ensuite à Conversions de fonctions anonymes :

Une expression lambda F est compatible avec un type d'arbre d'expression Expression si F est compatible avec le type de délégué D . Notez que cela ne s'applique pas aux méthodes anonymes, uniquement aux expressions lambda.

Ce sont les extraits secs de la spécification. Cependant, il est également utile de lire Comment s'assurer que l'inférence de type se termine pour rassembler des bits.


0 commentaires

3
votes

La proximité des classes héritées compte plus que les types de paramètres de méthode exacts

Notez que la variante Expression> s'applique à IQueryable , alors que la variante Func s'applique à IEnumerable .
Lors de la recherche d'une méthode correspondante, le compilateur choisira toujours celle la plus proche du type de l'objet. La hiérarchie d'héritage est la suivante:

DbSet<T> : IQueryable<T> : IEnumerable<T>

Remarque: il peut y avoir d'autres héritages entre les deux, mais cela n'a pas d'importance. Ce qui compte, c'est ce qui est le plus proche de DbSet . IQueryable est plus proche de DbSet que de IEnumerable.

Par conséquent, le compilateur essaiera de trouver une méthode correspondante dans IQueryable . Il pose deux questions:

  • Ce type a-t-il une méthode portant ce nom?
  • Les types de paramètres de méthode correspondent-ils / mappent-ils?

IQueryable a une méthode FirstOrDefault , donc le point 1 est satisfait); et comme x => x.MyBoolean peut être implicitement converti en Expression > , le point 2 est également satisfait.

Par conséquent, vous vous retrouvez avec la variante Expression> définie sur IQueryable.

Supposons que x => x.MyBoolean ne puisse pas être implicitement converti en Expression> mais pourrait être converti en Func (remarque: ce n'est pas le cas, mais cela pourrait arriver pour d'autres types / valeurs), alors la puce 2 n'aurait pas été satisfait.
À ce stade, comme le compilateur n'a pas trouvé de correspondance dans IQueryable , il continuera à chercher plus loin, trébuchant sur IEnumerable et se posera les mêmes questions (puces). Les deux puces auraient été satisfaites.

Par conséquent, dans ce cas, vous auriez fini avec la variante Func définie sur IEnumerable .

Mise à jour

Voici un exemple dotnetfiddle a>.

Notez que même si je passe des valeurs int (que la signature de la méthode de base utilise), la signature double du Derived correspond à la classe (car int se convertit implicitement en double ) et le compilateur ne regarde jamais dans la classe Base .

Cependant, ce n'est pas vrai dans Derived2 . Puisque int ne se convertit pas implicitement en string , il n'y a pas de correspondance trouvée dans Derived2 , et le compilateur regarde plus loin dans Base code> et utilise la méthode int de Base.


3 commentaires

Voici maintenant une question qui pourrait énerver un peu votre cerveau. Supposons que Base a une méthode public void M (Derived) et Derived a une méthode public void M (Base) . Si vous appelez dérivé.M (dérivé) , quelle méthode est appelée, Base.M ou Derived.M et pourquoi? La réponse peut vous surprendre, mais elle a du bon sens.


@EricLippert En suivant la même logique, je dirais que Derived.M doit être appelé (puisque Derived se convertit implicitement en Base , similaire à mon < code> double vs int exemple violon) et qui semble être vrai . Sur la base de votre commentaire, je commençais à m'attendre à ce que ce soit le contraire :)


La plupart des gens oublient de regarder le récepteur et ne regardent que les arguments; basé uniquement sur les arguments auxquels vous vous attendez à ce que Base.M (Derived) soit appelé car l'argument est plus spécifique, mais C # considère que la spécificité du récepteur est plus important que la spécificité de l ' argument car l'implémentation que vous appelez est à l'intérieur du récepteur!



4
votes

La réponse acceptée est une explication raisonnable, mais j'ai pensé que je pourrais fournir un peu plus de détails.

Disons que vous utilisez FirstOrDefault sur un DbSet. DbContext.Foos.FirstOrDefault (x => x.Bar == true);

Tout d'abord, j'espère que vous n'écririez pas cela. Si vous voulez demander "il pleut?" demandez-vous "il pleut?" ou demandez-vous "est-ce que l'affirmation qu'il pleut une vraie déclaration?" Dites simplement FirstOrDefault (x => x.Bar) .

Ensuite, compte tenu de ces surcharges:

public static TSource FirstOrDefault<TSource>(
    this IQueryable<TSource> source, 
    Expression<Func<TSource, bool>> predicate)

public static TSource FirstOrDefault<TSource>(
    this IEnumerable<TSource> source, 
    Func<TSource, bool> predicate)

Comment le compilateur choisir quelle surcharge est la meilleure?

Tout d'abord, nous faisons une inférence de type pour déterminer ce que TSource est dans chacun. Les détails de l'algorithme d'inférence de type sont complexes; posez une question plus ciblée si vous avez une question à ce sujet.

Si l'inférence de type ne parvient pas à déterminer un type pour TSource dans l'un ou l'autre, la méthode d'inférence ayant échoué est écartée de l'ensemble des candidats. Dans votre exemple, TSource peut être déterminé comme étant Foo , vraisemblablement.

Ensuite, parmi les candidats qui restent, nous les vérifions pour l'applicabilité d'arguments aux formels . Autrement dit, pouvons-nous convertir chaque argument fourni en son type de paramètre formel correspondant? (Et bien sûr, le nombre d'arguments fournis est-il correct, et ainsi de suite.) Dans votre exemple, les deux méthodes sont applicables.

Parmi les candidats applicables qui restent, nous entrons maintenant une ronde de amertume vérification . Comment fonctionne la vérification de l'éternité? Encore une fois, nous le faisons argument par argument. Dans ce cas, nous avons deux questions à répondre:

  • DbContext.Foos peut être converti en IEnumerable ou IQueryable . Quelle est la meilleure conversion, le cas échéant?
  • Le lambda peut être converti en délégué ou en arborescence d'expression. Quelle est la meilleure conversion, le cas échéant?

La deuxième question est facile à répondre: ni l'un ni l'autre n'est mieux. Nous n'apprendrons rien de cet argument en ce qui concerne l'amertume.

Pour répondre à la première question, nous appliquons la règle la conversion en spécifique est meilleure que la conversion en général . Si vous avez le choix de se convertir en girafe ou en mammifère, il est préférable de se convertir en girafe. Alors maintenant, la question est de savoir qui est plus spécifique , IQueryable ou IEnumerable?

La règle de la vérification de spécificité est simple: si X peut être implicitement converti en Y mais Y ne peut pas être implicitement converti en X, alors X est le plus spécifique. Une girafe peut être utilisée là où un animal est nécessaire, mais un animal ne peut pas être utilisé là où une girafe est nécessaire, donc Girafe est plus spécifique. Ou: chaque girafe est un animal, mais tous les animaux ne sont pas une girafe, donc la girafe est plus spécifique.

Par cette mesure, IQueryable est plus spécifique que IEnumerable car chaque objet interrogeable est un énumérable mais tous les énumérables ne sont pas interrogeables.

Ainsi, l'objet interrogeable est plus spécifique, et donc cette conversion est meilleure.

Maintenant, nous posons la question "Y a-t-il une méthode candidate unique applicable par rapport à chaque autre candidat, au moins une conversion était meilleure et aucune conversion n'était pire ? " Il y a; le candidat interrogeable a la propriété qu'il est meilleur dans un argument que tous les autres, et pas pire dans tous les autres arguments, et c'est la méthode unique qui a cette propriété.

Par conséquent, la résolution de surcharge choisit cette méthode.

Je vous encourage à lire la spécification si vous avez d'autres questions.


2 commentaires

C'est beaucoup d'informations utiles et intéressantes, je l'apprécie vraiment. Je n'aurais jamais pensé qu'il y aurait autant de logique derrière ça!


@Twenty: De rien. Nous nous sommes efforcés d'avoir des principes dans la conception de l'algorithme.