konwertowanie .net Func <T> na .net Expression <Func <T>>

118

Przejście od lambdy do wyrażenia jest łatwe przy użyciu wywołania metody ...

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

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

Ale chciałbym zmienić Func w wyrażenie, tylko w rzadkich przypadkach ...

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

Linia, która nie działa, wyświetla błąd w czasie kompilacji Cannot implicitly convert type 'System.Func<T>' to 'System.Linq.Expressions.Expression<System.Func<T>>'. Jawna obsada nie rozwiązuje sytuacji. Czy istnieje możliwość zrobienia tego, czego nie zauważam?

Dave Cameron
źródło
Naprawdę nie widzę pożytku z przykładu „rzadkiego przypadku”. Wzywający przekazuje w Func <T>. Nie ma potrzeby powtarzania wywołującemu, czym była ta Func <T> (poprzez wyjątek).
Adam Ralph,
2
W programie wywołującym nie jest obsługiwany wyjątek. A ponieważ istnieje wiele witryn wywołujących przekazujących różne funkcje Func <T>, przechwycenie wyjątku w obiekcie wywołującym tworzy duplikację.
Dave Cameron
1
Ślad stosu wyjątków służy do pokazywania tych informacji. Jeśli wyjątek zostanie zgłoszony w ramach wywołania Func <T>, zostanie to pokazane w śladzie stosu. Nawiasem mówiąc, gdybyś zdecydował się pójść w drugą stronę, tj. Zaakceptować wyrażenie i skompilować je do wywołania, straciłbyś to, ponieważ ślad stosu pokazywałby coś podobnego at lambda_method(Closure )do wywołania skompilowanego delegata.
Adam Ralph
Myślę, że powinieneś spojrzeć na odpowiedź w tym [link] [1] [1]: stackoverflow.com/questions/9377635/create-expression-from-func/…
Ibrahim Kais Ibrahim

Odpowiedzi:

104

Ooh, to wcale nie jest łatwe. Func<T>reprezentuje rodzaj, delegatea nie wyrażenie. Jeśli można to zrobić w jakikolwiek sposób (z powodu optymalizacji i innych rzeczy wykonanych przez kompilator, niektóre dane mogą zostać wyrzucone, więc odzyskanie oryginalnego wyrażenia może być niemożliwe), byłby to demontaż IL w locie i wywnioskować wyrażenie (co wcale nie jest łatwe). Traktowanie wyrażeń lambda jako danych ( Expression<Func<T>>) jest magią wykonywaną przez kompilator (w zasadzie kompilator buduje drzewo wyrażeń w kodzie zamiast kompilować je do IL).

Powiązany fakt

Dlatego języki, które wypychają lambdy do skrajności (takie jak Lisp), są często łatwiejsze do zaimplementowania jako interpretery . W tych językach kod i dane są zasadniczo tym samym (nawet w czasie wykonywania ), ale nasz chip nie może zrozumieć tej formy kodu, więc musimy emulować taką maszynę, budując na niej interpreter, który ją rozumie ( wybór dokonany przez Lisp podobnie jak języki) lub poświęcenie mocy (kod nie będzie już dokładnie równy danym) do pewnego stopnia (wybór dokonany przez C #). W języku C # kompilator daje złudzenie traktowania kodu jako danych, umożliwiając interpretowanie wyrażeń lambda jako code ( Func<T>) i data ( Expression<Func<T>>) w czasie kompilacji .

Mehrdad Afshari
źródło
3
Lispa nie trzeba interpretować, można go łatwo skompilować. Makra musiałyby zostać rozszerzone w czasie kompilacji, a jeśli chcesz je obsługiwać eval, musisz uruchomić kompilator, ale poza tym nie ma z tym żadnego problemu.
konfigurator
2
"Expression <Func <T>> DangerousExpression = () => dangerousCall ();" nie jest łatwe?
mheyman
10
@mheyman Stworzyłoby to nowe Expressioninformacje o akcji opakowania, ale nie zawierałoby informacji o drzewie wyrażeń o elementach wewnętrznych dangerousCalldelegata.
Nenad
34
    private static Expression<Func<T, bool>> FuncToExpression<T>(Func<T, bool> f)  
    {  
        return x => f(x);  
    } 
Nadpisanie
źródło
1
Chciałem przejść przez drzewo składni zwróconego wyrażenia. Czy takie podejście pozwoliłoby mi to zrobić?
Dave Cameron
6
@DaveCameron - Nie. Patrz powyższe odpowiedzi - już skompilowane Funczostaną ukryte w nowym wyrażeniu. To po prostu dodaje jedną warstwę danych do kodu; możesz przejść przez jedną warstwę tylko po to, aby znaleźć swój parametr fbez dalszych szczegółów, więc jesteś od punktu wyjścia.
Jonno
21

To, co prawdopodobnie powinieneś zrobić, to odwrócić tę metodę. Weź Expression>, skompiluj i uruchom. Jeśli to się nie powiedzie, masz już wyrażenie do sprawdzenia.

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

Oczywiście musisz wziąć pod uwagę wpływ tego na wydajność i określić, czy jest to coś, co naprawdę musisz zrobić.

David Wengier
źródło
7

Możesz jednak przejść w drugą stronę za pomocą metody .Compile () - nie jestem pewien, czy jest to przydatne:

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
źródło
6

Jeśli czasami potrzebujesz wyrażenia, a czasami potrzebujesz pełnomocnika, masz dwie opcje:

  • mają różne metody (po 1 dla każdego)
  • zawsze akceptuj Expression<...>wersję i po prostu .Compile().Invoke(...)ją, jeśli chcesz delegata. Oczywiście to kosztuje.
Marc Gravell
źródło
6

NJection.LambdaConverter to biblioteka konwertująca delegatów na wyrażenie

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
źródło
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
źródło
Czy możesz rozwinąć część „to nie zadziała”? Czy faktycznie próbowałeś go skompilować i wykonać? A może nie działa szczególnie w Twojej aplikacji?
Dmitry Dzygin
1
FWIW, może nie o to chodziło w głównym bilecie, ale tego potrzebowałem. To była ta call.Targetczęść, która mnie zabijała. Działał przez lata, a potem nagle przestał działać i zaczął narzekać na statyczne / niestatyczne bla bla. W każdym razie dzięki!
Eli Gassert
-1

Zmiana

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

Do

// This next line works!
Expression<Func<T>> DangerousExpression = () => dangerousCall();
mheyman
źródło
Servy, jest to całkowicie legalny sposób na uzyskanie ekspresji. cukier składniowy, aby go zbudować za pomocą expression.lambda i expression.call. Jak myślisz, dlaczego powinno to zawieść w czasie wykonywania?
Roman Pokrovskij