Pourquoi utiliseriez-vous Expression <Func <T>> plutôt que Func <T>?

949

Je comprends les lambdas et le FuncetAction les délégués. Mais les expressions me frappent.

Dans quelles circonstances utiliseriez-vous Expression<Func<T>>un ancien plutôt qu'un simple ancien Func<T>?

Richard Nagle
la source
14
Func <> sera converti en une méthode au niveau du compilateur c #, Expression <Func <>> sera exécuté au niveau MSIL après avoir compilé le code directement, c'est la raison pour laquelle il est plus rapide
Waleed AK
1
en plus des réponses, la spécification du langage csharp "4.6 types d'arbres d'expression" est utile pour faire des références croisées
djeikyb

Réponses:

1133

Lorsque vous souhaitez traiter les expressions lambda comme des arborescences d'expressions et regarder à l'intérieur au lieu de les exécuter. Par exemple, LINQ to SQL obtient l'expression et la convertit en instruction SQL équivalente et la soumet au serveur (plutôt que d'exécuter le lambda).

Conceptuellement, Expression<Func<T>>est complètement différent de Func<T>. Func<T>dénote un delegatequi est à peu près un pointeur vers une méthode et Expression<Func<T>>dénote une structure de données d'arbre pour une expression lambda. Cette structure arborescente décrit ce qu'une expression lambda fait plutôt que de faire la chose réelle. Il contient essentiellement des données sur la composition des expressions, des variables, des appels de méthode, ... (par exemple, il contient des informations telles que ce lambda est une constante + un paramètre). Vous pouvez utiliser cette description pour la convertir en une méthode réelle (avec Expression.Compile) ou faire d'autres choses (comme l'exemple LINQ to SQL) avec. Le fait de traiter les lambdas comme des méthodes anonymes et des arbres d'expression est purement une chose au moment de la compilation.

Func<int> myFunc = () => 10; // similar to: int myAnonMethod() { return 10; }

compilera efficacement vers une méthode IL qui n'obtient rien et renvoie 10.

Expression<Func<int>> myExpression = () => 10;

sera converti en une structure de données qui décrit une expression qui n'obtient aucun paramètre et renvoie la valeur 10:

Expression vs Func une plus grande image

Bien qu'ils se ressemblent tous les deux au moment de la compilation, ce que le compilateur génère est totalement différent .

Mehrdad Afshari
la source
96
Donc, en d'autres termes, an Expressioncontient les méta-informations sur un certain délégué.
bertl
40
@bertl En fait, non. Le délégué n'est pas du tout impliqué. La raison pour laquelle il existe une association avec un délégué est que vous pouvez compiler l'expression vers un délégué - ou, pour être plus précis, la compiler dans une méthode et obtenir le délégué de cette méthode comme valeur de retour. Mais l'arbre d'expression lui-même n'est que des données. Le délégué n'existe pas lorsque vous utilisez Expression<Func<...>>au lieu de simplement Func<...>.
Luaan
5
@Kyle Delaney (isAnExample) => { if(isAnExample) ok(); else expandAnswer(); } telle expression est un ExpressionTree, des branches sont créées pour l'instruction If.
Matteo Marciano - MSCP
3
@bertl Delegate est ce que le CPU voit (code exécutable d'une architecture), Expression est ce que le compilateur voit (simplement un autre format de code source, mais toujours du code source).
codewarrior
5
@bertl: Cela pourrait être résumé plus précisément en disant qu'une expression est à un func ce qu'un stringbuilder est à une chaîne. Ce n'est pas une chaîne / func, mais elle contient les données nécessaires pour en créer une lorsque vous y êtes invité.
Flater
337

J'ajoute une réponse pour les noobs parce que ces réponses semblaient sur ma tête, jusqu'à ce que je réalise à quel point c'est simple. Parfois, vous vous attendez à ce que ce soit compliqué qui vous empêche de «vous envelopper la tête».

Je n'avais pas besoin de comprendre la différence jusqu'à ce que j'entre dans un `` bogue '' vraiment ennuyeux essayant d'utiliser LINQ-to-SQL de manière générique:

public IEnumerable<T> Get(Func<T, bool> conditionLambda){
  using(var db = new DbContext()){
    return db.Set<T>.Where(conditionLambda);
  }
}

Cela a très bien fonctionné jusqu'à ce que je commence à obtenir OutofMemoryExceptions sur des ensembles de données plus importants. Définir des points d'arrêt à l'intérieur du lambda m'a fait réaliser qu'il parcourait chaque ligne de ma table un par un à la recherche de correspondances avec ma condition lambda. Cela m'a bloqué pendant un certain temps, car pourquoi diable traite-t-il ma table de données comme un géant IEnumerable au lieu de faire LINQ-to-SQL comme il est censé le faire? Il faisait également exactement la même chose dans mon homologue LINQ-to-MongoDb.

Le correctif était simplement de Func<T, bool>devenir Expression<Func<T, bool>>, alors j'ai cherché pourquoi il fallait un Expressionau lieu de Func, pour finir ici.

Une expression transforme simplement un délégué en données sur lui-même.Devient a => a + 1alors quelque chose comme "Sur le côté gauche, il y a un int a. Sur le côté droit, vous ajoutez 1". C'est ça. Tu peux rentrer chez toi maintenant. C'est évidemment plus structuré que cela, mais c'est essentiellement tout ce qu'un arbre d'expression est vraiment - rien pour vous envelopper.

En comprenant cela, il devient clair pourquoi LINQ-to-SQL a besoin d'un Expression et un Funcn'est pas adéquat. Funcne porte pas avec lui un moyen d'entrer en lui-même, de voir le détail de la façon de le traduire en une requête SQL / MongoDb / autre. Vous ne pouvez pas voir s'il s'agit d'addition, de multiplication ou de soustraction. Tout ce que vous pouvez faire, c'est l'exécuter. Expression, d'autre part, vous permet de regarder à l'intérieur du délégué et de voir tout ce qu'il veut faire. Cela vous permet de traduire le délégué en ce que vous voulez, comme une requête SQL. Funcn'a pas fonctionné car mon DbContext était aveugle au contenu de l'expression lambda. Pour cette raison, il n'a pas pu transformer l'expression lambda en SQL; cependant, il a fait la meilleure chose suivante et a itéré cette conditionnelle à travers chaque ligne de ma table.

Edit: exposant ma dernière phrase à la demande de John Peter:

IQueryable étend IEnumerable, donc les méthodes IEnumerable comme Where()obtenir des surcharges qui acceptent Expression. Lorsque vous passez un Expressionà cela, vous conservez un IQueryable en conséquence, mais lorsque vous passez un Func, vous retombez sur le IEnumerable de base et vous obtiendrez un IEnumerable en conséquence. En d'autres termes, sans remarquer que vous avez transformé votre ensemble de données en liste à itérer plutôt qu'en quelque chose à interroger. Il est difficile de remarquer une différence jusqu'à ce que vous regardiez vraiment sous le capot les signatures.

Chad Hedgcock
la source
2
Tchad; Veuillez expliquer ce commentaire un peu plus: "Func n'a pas fonctionné parce que mon DbContext était aveugle à ce qui était réellement dans l'expression lambda pour le transformer en SQL, donc il a fait la meilleure chose suivante et a itéré cette conditionnelle à travers chaque ligne de ma table . "
John Peters
2
>> Func ... Tout ce que vous pouvez faire est de l'exécuter. Ce n'est pas tout à fait vrai, mais je pense que c'est le point à souligner. Les fonctions / actions doivent être exécutées, les expressions doivent être analysées (avant de s'exécuter ou même au lieu de s'exécuter).
Konstantin
@Chad Le problème est-il ici? . Je pense que vous obtenez OutOfMemoryException car, ce code a essayé de charger la table entière dans la mémoire (et bien sûr, a créé les objets). Ai-je raison? Merci :)
Bence Végert
104

Une considération extrêmement importante dans le choix d'Expression vs Func est que les fournisseurs IQueryable comme LINQ to Entities peuvent `` digérer '' ce que vous passez dans une Expression, mais ignorent ce que vous passez dans un Func. J'ai deux articles de blog sur le sujet:

En savoir plus sur Expression vs Func avec Entity Framework et Falling in Love with LINQ - Partie 7: Expressions et Funcs (la dernière section)

LSpencer777
la source
+ l pour l'explication. Cependant, j'obtiens "Le type de nœud d'expression LINQ" Invoke "n'est pas pris en charge dans LINQ to Entities." et a dû utiliser ForEach après avoir récupéré les résultats.
tymtam
77

Je voudrais ajouter quelques notes sur les différences entre Func<T>et Expression<Func<T>>:

  • Func<T> est juste un MulticastDelegate old-school normal;
  • Expression<Func<T>> est une représentation de l'expression lambda sous forme d'arbre d'expression;
  • l'arborescence d'expression peut être construite via la syntaxe d'expression lambda ou via la syntaxe API;
  • l'arbre d'expression peut être compilé pour un délégué Func<T> ;
  • la conversion inverse est théoriquement possible, mais c'est une sorte de décompilation, il n'y a pas de fonctionnalité intégrée pour cela car ce n'est pas un processus simple;
  • arbre d'expression peut être observé / traduit / modifié par le ExpressionVisitor ;
  • les méthodes d'extension pour IEnumerable fonctionnent avec Func<T> ;
  • les méthodes d'extension pour IQueryable fonctionnent avec Expression<Func<T>>.

Il y a un article qui décrit les détails avec des exemples de code:
LINQ: Func <T> vs. Expression <Func <T>> .

J'espère que ce sera utile.

Olexander Ivanitskyi
la source
Belle liste, une petite note est que vous mentionnez que la conversion inverse est possible, mais pas exactement l'inverse. Certaines métadonnées sont perdues pendant le processus de conversion. Cependant, vous pouvez le décompiler dans un arbre d'expression qui produit le même résultat lors de la nouvelle compilation.
Aidiakapi
76

Il y a une explication plus philosophique à ce sujet dans le livre de Krzysztof Cwalina ( Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries );

Rico Mariani

Modifier pour une version sans image:

La plupart du temps, vous voudrez Func ou Action si tout ce qui doit arriver est d'exécuter du code. Vous avez besoin d' Expression lorsque le code doit être analysé, sérialisé ou optimisé avant d'être exécuté. L'expression est pour penser au code, Func / Action est pour l'exécuter.

Oğuzhan Soykan
la source
10
Bien placé. c'est à dire. Vous avez besoin d'expression lorsque vous vous attendez à ce que votre Func soit converti en une sorte de requête. C'est à dire. vous devez database.data.Where(i => i.Id > 0)être exécuté en tant que SELECT FROM [data] WHERE [id] > 0. Si vous passez juste un Func, vous avez mis des œillères sur votre pilote et tout ce qu'il peut faire est SELECT *et une fois qu'il est chargé toutes ces données en mémoire, itérer à travers chaque filtre et à tout avec id> 0. Recouvrez le Funcdans Expressionresponsabilise le pilote pour analyser le Funcet le transformer en une requête SQL / MongoDb / autre.
Chad Hedgcock
Donc, quand je ExpressionFunc/Action
prévois des
1
@ChadHedgcock C'était la dernière pièce dont j'avais besoin. Merci. Je regarde cela depuis un moment, et votre commentaire ici a fait claquer toute l'étude.
johnny
37

LINQ est l'exemple canonique (par exemple, parler à une base de données), mais en vérité, chaque fois que vous vous souciez plus d'exprimer quoi faire, plutôt que de le faire réellement. Par exemple, j'utilise cette approche dans la pile RPC de protobuf-net (pour éviter la génération de code, etc.) - vous appelez donc une méthode avec:

string result = client.Invoke(svc => svc.SomeMethod(arg1, arg2, ...));

Cela déconstruit l'arborescence d'expressions à résoudre SomeMethod(et la valeur de chaque argument), effectue l'appel RPC, met à jour tout ref/ outargs et renvoie le résultat de l'appel distant. Cela n'est possible que via l'arbre d'expression. Je couvre cela plus ici .

Un autre exemple est lorsque vous créez manuellement les arborescences d'expression dans le but de les compiler en lambda, comme le fait le code des opérateurs génériques .

Marc Gravell
la source
20

Vous utiliseriez une expression lorsque vous souhaitez traiter votre fonction comme des données et non comme du code. Vous pouvez le faire si vous souhaitez manipuler le code (en tant que données). La plupart du temps, si vous ne voyez pas le besoin d'expressions, vous n'avez probablement pas besoin d'en utiliser une.

Andrew Hare
la source
19

La principale raison est que vous ne souhaitez pas exécuter le code directement, mais plutôt l'inspecter. Cela peut être pour un certain nombre de raisons:

  • Mappage du code à un environnement différent (par exemple, du code C # à SQL dans Entity Framework)
  • Remplacement de parties du code lors de l'exécution (programmation dynamique ou même techniques DRY simples)
  • Validation du code (très utile lors de l'émulation de scripts ou lors de l'analyse)
  • Sérialisation - les expressions peuvent être sérialisées assez facilement et en toute sécurité, les délégués ne peuvent pas
  • Sécurité fortement typée sur les choses qui ne sont pas intrinsèquement fortement typées et exploitant les vérifications du compilateur même si vous effectuez des appels dynamiques pendant l'exécution (ASP.NET MVC 5 avec Razor est un bel exemple)
Luaan
la source
pouvez-vous en dire un peu plus sur le n ° 5
uowzd01
@ uowzd01 Il suffit de regarder Razor - il utilise largement cette approche.
Luaan
@Luaan Je recherche des sérialisations d'expression mais je ne trouve rien sans une utilisation limitée par un tiers. .Net 4.5 prend-il en charge la sérialisation de l'arborescence d'expressions?
vabii
@vabii Pas que je sache - et ce ne serait pas vraiment une bonne idée pour le cas général. Mon point était plus sur le fait que vous puissiez écrire une sérialisation assez simple pour les cas spécifiques que vous souhaitez prendre en charge, contre des interfaces conçues à l'avance - je l'ai fait à quelques reprises. Dans le cas général, un Expressionpeut être tout aussi impossible à sérialiser qu'un délégué, car toute expression peut contenir une invocation d'une référence arbitraire de délégué / méthode. "Facile" est bien sûr relatif.
Luaan
15

Je ne vois pas encore de réponses qui mentionnent la performance. Passer ou Func<>s est mauvais. Vraiment mauvais. Si vous utilisez un, il appelle la substance LINQ à la place de , ce qui signifie que des tables entières sont récupérées puis filtrées. est beaucoup plus rapide, surtout si vous interrogez une base de données qui vit sur un autre serveur.Where()Count()Func<>IEnumerableIQueryableExpression<Func<>>

mhenry1384
la source
Cela s'applique-t-il également aux requêtes en mémoire?
stt106
@ stt106 Probablement pas.
mhenry1384
Cela n'est vrai que si vous énumérez la liste. Si vous utilisez GetEnumerator ou foreach, vous ne chargerez pas entièrement l'ienumerable en mémoire.
nelsontruran
1
@ stt106 Lorsqu'elle est passée à la clause .Where () d'une liste <>, l'expression <Func <>> est appelée .Compile (), donc Func <> est presque certainement plus rapide. Voir referencesource.microsoft.com/#System.Core/System/Linq/…
NStuke