conversion d'un .net Func <T> en une expression .net <Func <T>>

118

Passer d'un lambda à une expression est facile en utilisant un appel de méthode ...

public void GimmeExpression(Expression<Func<T>> expression)
{
    ((MemberExpression)expression.Body).Member.Name; // "DoStuff"
}

public void SomewhereElse()
{
    GimmeExpression(() => thing.DoStuff());
}

Mais je voudrais transformer le Func en une expression, seulement dans de rares cas ...

public void ContainTheDanger(Func<T> dangerousCall)
{
    try 
    {
        dangerousCall();
    }
    catch (Exception e)
    {
        // This next line does not work...
        Expression<Func<T>> DangerousExpression = dangerousCall;
        var nameOfDanger = 
            ((MemberExpression)dangerousCall.Body).Member.Name;
        throw new DangerContainer(
            "Danger manifested while " + nameOfDanger, e);
    }
}

public void SomewhereElse()
{
    ContainTheDanger(() => thing.CrossTheStreams());
}

La ligne qui ne fonctionne pas me donne l'erreur de compilation Cannot implicitly convert type 'System.Func<T>' to 'System.Linq.Expressions.Expression<System.Func<T>>'. Un casting explicite ne résout pas la situation. Y a-t-il une possibilité de faire cela que je néglige?

Dave Cameron
la source
Je ne vois pas vraiment d'utilité pour l'exemple du «cas rare». L'appelant passe le Func <T>. Il n'est pas nécessaire de répéter à l'appelant ce qu'était ce Func <T> (via l'exception).
Adam Ralph
2
L'exception n'est pas gérée dans l'appelant. Et, comme il existe plusieurs sites d'appel passant dans différents Func <T>, intercepter l'exception dans l'appelant crée une duplication.
Dave Cameron
1
La trace de pile d'exceptions est conçue pour afficher ces informations. Si l'exception est levée lors de l'appel de Func <T>, cela apparaîtra dans la trace de la pile. Incidemment, si vous décidiez d'aller dans l'autre sens, c'est-à-dire d'accepter une expression et de la compiler pour l'invocation, vous perdriez cela puisque la trace de la pile afficherait quelque chose comme at lambda_method(Closure )pour l'invocation du délégué compilé.
Adam Ralph
Je suppose que vous devriez regarder la réponse dans ce [lien] [1] [1]: stackoverflow.com/questions/9377635/create-expression-from-func/…
Ibrahim Kais Ibrahim

Réponses:

104

Ooh, ce n'est pas du tout facile. Func<T>représente un générique delegateet non une expression. S'il y a un moyen de le faire (en raison des optimisations et d'autres choses effectuées par le compilateur, certaines données peuvent être jetées, il peut donc être impossible de récupérer l'expression d'origine), ce serait désassembler l'IL à la volée et inférer l'expression (ce qui n'est en aucun cas facile). Traiter les expressions lambda comme des données ( Expression<Func<T>>) est une magie faite par le compilateur (en gros, le compilateur construit un arbre d'expression dans le code au lieu de le compiler en IL).

Fait connexe

C'est pourquoi les langages qui poussent les lambdas à l'extrême (comme Lisp) sont souvent plus faciles à implémenter en tant qu'interprètes . Dans ces langages, le code et les données sont essentiellement la même chose (même au moment de l'exécution ), mais notre puce ne peut pas comprendre cette forme de code, nous devons donc émuler une telle machine en construisant au-dessus un interpréteur qui la comprend (le choix fait par Lisp comme les langages) ou sacrifier la puissance (le code ne sera plus exactement égal aux données) dans une certaine mesure (le choix fait par C #). En C #, le compilateur donne l'illusion de traiter le code comme des données en permettant aux lambdas d'être interprétées comme code ( Func<T>) et data ( Expression<Func<T>>) au moment de la compilation .

Mehrdad Afshari
la source
3
Lisp n'a pas besoin d'être interprété, il peut être facilement compilé. Les macros devraient être développées au moment de la compilation, et si vous voulez prendre en charge, evalvous devrez démarrer le compilateur, mais à part cela, il n'y a aucun problème à le faire.
configurateur
2
"Expression <Func <T>> DangerousExpression = () => dangerousCall ();" n'est pas facile?
mheyman
10
@mheyman Cela créerait une nouvelle Expressionsur votre action de wrapper, mais il n'aurait aucune information d'arborescence d'expression sur les éléments internes du dangerousCalldélégué.
Nenad
34
    private static Expression<Func<T, bool>> FuncToExpression<T>(Func<T, bool> f)  
    {  
        return x => f(x);  
    } 
Passer outre
la source
1
Je voulais parcourir l'arbre de syntaxe de l'expression retournée. Cette approche me permettrait-elle de faire cela?
Dave Cameron
6
@DaveCameron - Non. Voir les réponses ci-dessus - le déjà compilé Funcsera caché dans une nouvelle expression. Cela ajoute simplement une couche de données sur le code; vous pouvez parcourir une couche juste pour trouver votre paramètre fsans plus de détails, vous êtes donc là où vous avez commencé.
Jonno
21

Ce que vous devriez probablement faire, c'est changer la méthode. Prenez une expression>, compilez et exécutez. En cas d'échec, vous avez déjà l'Expression à examiner.

public void ContainTheDanger(Expression<Func<T>> dangerousCall)
{
    try 
    {
        dangerousCall().Compile().Invoke();;
    }
    catch (Exception e)
    {
        // This next line does not work...
        var nameOfDanger = 
            ((MemberExpression)dangerousCall.Body).Member.Name;
        throw new DangerContainer(
            "Danger manifested while " + nameOfDanger, e);
    }
}

public void SomewhereElse()
{
    ContainTheDanger(() => thing.CrossTheStreams());
}

De toute évidence, vous devez tenir compte de ses implications en termes de performances et déterminer si c'est quelque chose que vous devez vraiment faire.

David Wengier
la source
7

Vous pouvez cependant aller dans l'autre sens via la méthode .Compile () - vous ne savez pas si cela vous est utile:

public void ContainTheDanger<T>(Expression<Func<T>> dangerousCall)
{
    try
    {
        var expr = dangerousCall.Compile();
        expr.Invoke();
    }
    catch (Exception e)
    {
        Expression<Func<T>> DangerousExpression = dangerousCall;
        var nameOfDanger = ((MethodCallExpression)dangerousCall.Body).Method.Name;
        throw new DangerContainer("Danger manifested while " + nameOfDanger, e);
    }
}

public void SomewhereElse()
{
    var thing = new Thing();
    ContainTheDanger(() => thing.CrossTheStreams());
}
Steve Willcock
la source
6

Si vous avez parfois besoin d'une expression et parfois besoin d'un délégué, vous avez 2 options:

  • ont des méthodes différentes (1 pour chacune)
  • acceptez toujours la Expression<...>version, et uniquement .Compile().Invoke(...)si vous voulez un délégué. Évidemment, cela a coûté.
Marc Gravell
la source
6

NJection.LambdaConverter est une bibliothèque qui convertit les délégués en expression

public class Program
{
    private static void Main(string[] args) {
       var lambda = Lambda.TransformMethodTo<Func<string, int>>()
                          .From(() => Parse)
                          .ToLambda();            
    }   

    public static int Parse(string value) {
       return int.Parse(value)
    } 
}
Sagi
la source
4
 Expression<Func<T>> ToExpression<T>(Func<T> call)
        {
            MethodCallExpression methodCall = call.Target == null
                ? Expression.Call(call.Method)
                : Expression.Call(Expression.Constant(call.Target), call.Method);

            return Expression.Lambda<Func<T>>(methodCall);
        }
Dmitry Dzygin
la source
Pouvez-vous élaborer la partie «cela ne fonctionnera pas»? Avez-vous réellement essayé de le compiler et de l'exécuter? Ou cela ne fonctionne pas particulièrement dans votre application?
Dmitry Dzygin
1
FWIW, ce n'était peut-être pas le sujet du ticket principal, mais c'était ce dont j'avais besoin. C'était la call.Targetpartie qui me tuait. Cela a fonctionné pendant des années, puis a soudainement cessé de fonctionner et a commencé à se plaindre d'un bla bla statique / non statique. Bref, merci!
Eli Gassert
-1

Changement

// This next line does not work...
Expression<Func<T>> DangerousExpression = dangerousCall;

À

// This next line works!
Expression<Func<T>> DangerousExpression = () => dangerousCall();
mheyman
la source
Servy, c'est un moyen absolument légal d'obtenir une expression. sucre de syntaxe pour le construire via expression.lambda et expression.call. Pourquoi pensez-vous qu'il devrait échouer lors de l'exécution?
Roman Pokrovskij