Konvertieren eines .net Func <T> in einen .net Ausdruck <Func <T>>

118

Mit einem Methodenaufruf ist es einfach, von einem Lambda zu einem Ausdruck zu wechseln ...

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

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

Aber ich möchte den Func nur in seltenen Fällen in einen Ausdruck verwandeln ...

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());
}

Die Zeile, die nicht funktioniert, gibt mir den Fehler beim Kompilieren Cannot implicitly convert type 'System.Func<T>' to 'System.Linq.Expressions.Expression<System.Func<T>>'. Eine explizite Besetzung löst die Situation nicht. Gibt es eine Möglichkeit, dies zu tun, die ich übersehen habe?

Dave Cameron
quelle
Ich sehe nicht wirklich viel Verwendung für das Beispiel "seltener Fall". Der Anrufer übergibt die Funktion <T>. Es ist nicht erforderlich, dem Anrufer zu wiederholen, was dieser Func <T> war (über die Ausnahme).
Adam Ralph
2
Die Ausnahme wird im Aufrufer nicht behandelt. Da mehrere Anrufseiten in unterschiedlichen Funktionen übergeben werden, führt das Abfangen der Ausnahme im Anrufer zu einer Duplizierung.
Dave Cameron
1
Der Exception-Stack-Trace soll diese Informationen anzeigen. Wenn die Ausnahme beim Aufruf von Func <T> ausgelöst wird, wird dies im Stack-Trace angezeigt. Wenn Sie sich für den anderen Weg entscheiden, dh einen Ausdruck akzeptieren und für den Aufruf kompilieren, verlieren Sie diesen übrigens, da der Stack-Trace so etwas wie at lambda_method(Closure )für den Aufruf des kompilierten Delegaten anzeigt.
Adam Ralph
Ich denke, Sie sollten sich die Antwort in diesem [Link] [1] [1] ansehen
Ibrahim Kais Ibrahim

Antworten:

104

Oh, es ist überhaupt nicht einfach. Func<T>repräsentiert einen generischen delegateund keinen Ausdruck. Wenn Sie dies auf irgendeine Weise tun können (aufgrund von Optimierungen und anderen vom Compiler vorgenommenen Vorgängen werden möglicherweise einige Daten weggeworfen, sodass der ursprüngliche Ausdruck möglicherweise nicht wiederhergestellt werden kann), wird die IL im laufenden Betrieb zerlegt und auf den Ausdruck schließen (was keineswegs einfach ist). Das Behandeln von Lambda-Ausdrücken als data ( Expression<Func<T>>) ist eine Magie des Compilers (im Grunde erstellt der Compiler einen Ausdrucksbaum im Code, anstatt ihn in IL zu kompilieren).

Verwandte Tatsache

Aus diesem Grund sind Sprachen, die Lambdas auf die Spitze treiben (wie Lisp), als Dolmetscher oft einfacher zu implementieren . In diesen Sprachen sind Code und Daten im Wesentlichen dasselbe (auch zur Laufzeit ), aber unser Chip kann diese Form von Code nicht verstehen. Daher müssen wir eine solche Maschine emulieren, indem wir darauf einen Interpreter aufbauen, der sie versteht (the Auswahl von Lisp wie Sprachen) oder Einbußen bei der Leistung (Code entspricht nicht mehr genau den Daten) bis zu einem gewissen Grad (Auswahl von C #). In C # gibt der Compiler die Illusion, Code als Daten zu behandeln, indem Lambdas zur Kompilierungszeit als Code ( Func<T>) und Daten ( Expression<Func<T>>) interpretiert werden können .

Mehrdad Afshari
quelle
3
Lisp muss nicht interpretiert werden, es kann leicht kompiliert werden. Makros müssten zur Kompilierungszeit erweitert werden, und wenn Sie unterstützen möchten, evalmüssten Sie den Compiler starten, aber ansonsten gibt es überhaupt kein Problem damit.
Konfigurator
2
"Ausdruck <Func <T >> DangerousExpression = () => gefährlicheCall ();" Es ist nicht einfach?
Mheyman
10
@mheyman Das würde neue Informationen Expressionzu Ihrer Wrapper-Aktion erstellen , aber keine Informationen zum Ausdrucksbaum über die Interna des dangerousCallDelegaten enthalten.
Nenad
34
    private static Expression<Func<T, bool>> FuncToExpression<T>(Func<T, bool> f)  
    {  
        return x => f(x);  
    } 
Überschreiben
quelle
1
Ich wollte den Syntaxbaum des zurückgegebenen Ausdrucks durchlaufen. Würde mir dieser Ansatz erlauben, das zu tun?
Dave Cameron
6
@ DaveCameron - Nein. Siehe obige Antworten - die bereits kompilierten Funcwerden in einem neuen Ausdruck ausgeblendet. Dies fügt einfach eine Datenschicht über Code hinzu. Sie können eine Ebene durchlaufen, um Ihren Parameter fohne weitere Details zu finden, sodass Sie genau dort sind, wo Sie begonnen haben.
Jonno
21

Was Sie wahrscheinlich tun sollten, ist die Methode umzudrehen. Nehmen Sie einen Ausdruck> auf und kompilieren Sie ihn und führen Sie ihn aus. Wenn dies fehlschlägt, müssen Sie bereits den Ausdruck untersuchen.

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());
}

Natürlich müssen Sie die Auswirkungen auf die Leistung berücksichtigen und feststellen, ob Sie dies wirklich tun müssen.

David Wengier
quelle
7

Sie können jedoch auch über die .Compile () -Methode in die andere Richtung gehen - nicht sicher, ob dies für Sie nützlich ist:

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
quelle
6

Wenn Sie manchmal einen Ausdruck und manchmal einen Delegaten benötigen, haben Sie zwei Möglichkeiten:

  • unterschiedliche Methoden haben (jeweils 1)
  • Akzeptieren Sie immer die Expression<...>Version und nur .Compile().Invoke(...)dann, wenn Sie einen Delegaten möchten. Offensichtlich hat dies Kosten verursacht.
Marc Gravell
quelle
6

NJection.LambdaConverter ist eine Bibliothek, die Delegaten in Ausdruck konvertiert

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
quelle
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
quelle
Können Sie den Teil "Das wird nicht funktionieren" näher erläutern? Haben Sie tatsächlich versucht, es zu kompilieren und auszuführen? Oder funktioniert es nicht besonders in Ihrer Anwendung?
Dmitry Dzygin
1
FWIW, das war vielleicht nicht das, worum es beim Hauptticket ging, aber es war das, was ich brauchte. Es war der call.TargetTeil, der mich umbrachte. Es funktionierte jahrelang und hörte dann plötzlich auf zu arbeiten und begann sich über ein statisches / nicht statisches bla bla zu beschweren. Trotzdem danke!
Eli Gassert
-1

Veränderung

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

Zu

// This next line works!
Expression<Func<T>> DangerousExpression = () => dangerousCall();
Mheyman
quelle
Servy, es ist absolut legal, sich auszudrücken. Syntax Zucker, um es durch expression.lambda und expression.call zu erstellen. Warum sollte es Ihrer Meinung nach zur Laufzeit fehlschlagen?
Roman Pokrovskij