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?
3 Réponses :
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'expressionExpression
siF
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.
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:
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
.
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
.
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 a>. 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!
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? 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.
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.
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.