converter um Func .net <T> em uma Expressão .net <Func <T>>

118

Ir de um lambda para uma Expressão é fácil usando uma chamada de método ...

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

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

Mas eu gostaria de transformar o Func em uma expressão, apenas em casos raros ...

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

A linha que não funciona me dá o erro de tempo de compilação Cannot implicitly convert type 'System.Func<T>' to 'System.Linq.Expressions.Expression<System.Func<T>>'. Um elenco explícito não resolve a situação. Existe uma facilidade para fazer isso que estou esquecendo?

Dave Cameron
fonte
Não vejo muita utilidade para o exemplo do 'caso raro'. O chamador está transmitindo Func <T>. Não há necessidade de repetir para o chamador o que era Func <T> (por meio da exceção).
Adam Ralph
2
A exceção não é tratada no chamador. E, como há vários sites de chamada passando em Func <T> s diferentes, capturar a exceção no chamador cria duplicação.
Dave Cameron
1
O rastreamento de pilha de exceção foi projetado para mostrar essas informações. Se a exceção for lançada na invocação de Func <T>, isso será mostrado no rastreamento de pilha. A propósito, se você escolher seguir o outro caminho, ou seja, aceitar uma expressão e compilá-la para invocação, você perderia isso, pois o rastreamento de pilha mostraria algo como at lambda_method(Closure )a invocação do delegado compilado.
Adam Ralph
Eu acho que você deve olhar para a resposta neste [link] [1] [1]: stackoverflow.com/questions/9377635/create-expression-from-func/…
Ibrahim Kais Ibrahim

Respostas:

104

Ooh, não é nada fácil. Func<T>representa um genérico delegatee não uma expressão. Se houver alguma maneira de fazer isso (devido a otimizações e outras coisas feitas pelo compilador, alguns dados podem ser jogados fora, então pode ser impossível obter a expressão original de volta), seria desmontar o IL na hora e inferir a expressão (o que não é nada fácil). Tratar expressões lambda como data ( Expression<Func<T>>) é uma mágica feita pelo compilador (basicamente, o compilador constrói uma árvore de expressão no código em vez de compilá-la para IL).

Fato relacionado

É por isso que as linguagens que levam lambdas ao extremo (como Lisp) são geralmente mais fáceis de implementar como interpretadores . Nessas linguagens, código e dados são essencialmente a mesma coisa (mesmo em tempo de execução ), mas nosso chip não consegue entender essa forma de código, então temos que emular tal máquina construindo um interpretador em cima dela que a entenda (o escolha feita por linguagens como o Lisp) ou sacrificando o poder (o código não será mais exatamente igual aos dados) até certo ponto (a escolha feita pelo C #). No C #, o compilador dá a ilusão de tratar o código como dados, permitindo que lambdas sejam interpretados como code ( Func<T>) e data ( Expression<Func<T>>) no momento da compilação .

Mehrdad Afshari
fonte
3
Lisp não precisa ser interpretado, pode ser facilmente compilado. As macros teriam que ser expandidas em tempo de compilação e, se você quiser oferecer suporte eval, precisará inicializar o compilador, mas além disso, não há problema nenhum em fazer isso.
configurador
2
"Expression <Func <T>> DangerousExpression = () => relevantCall ();" não é fácil?
mheyman
10
@mheyman Isso criaria novas informações Expressionsobre sua ação de wrapper, mas não teria nenhuma informação de árvore de expressão sobre os internos do dangerousCalldelegado.
Nenad
34
    private static Expression<Func<T, bool>> FuncToExpression<T>(Func<T, bool> f)  
    {  
        return x => f(x);  
    } 
Sobrepor
fonte
1
Eu queria percorrer a árvore de sintaxe da expressão retornada. Essa abordagem me permitiria fazer isso?
Dave Cameron de
6
@DaveCameron - Não. Veja as respostas acima - o já compilado Funcficará oculto em uma nova Expressão. Isso simplesmente adiciona uma camada de dados sobre o código; você pode atravessar uma camada apenas para encontrar seu parâmetro fsem mais detalhes, então você está exatamente onde começou.
Jonno
21

O que você provavelmente deve fazer é inverter o método. Pegue um Expression>, compile e execute. Se falhar, você já tem a Expressão para examinar.

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

Obviamente, você precisa considerar as implicações disso no desempenho e determinar se é algo que você realmente precisa fazer.

David Wengier
fonte
7

No entanto, você pode fazer o contrário por meio do método .Compile () - não tenho certeza se isso é útil para você:

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

Se às vezes você precisa de uma expressão e às vezes de um delegado, você tem 2 opções:

  • têm métodos diferentes (1 para cada)
  • aceite sempre a Expression<...>versão, e apenas .Compile().Invoke(...)se quiser um delegado. Obviamente, isso tem um custo.
Marc Gravell
fonte
6

NJection.LambdaConverter é uma biblioteca que converte delegados em expressão

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
fonte
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
fonte
Você pode elaborar a parte "isso não vai funcionar"? Você realmente tentou compilá-lo e executá-lo? Ou não funciona particularmente em seu aplicativo?
Dmitry Dzygin de
1
FWIW, pode não ser sobre o que tratava o tíquete principal, mas era o que eu precisava. Era a call.Targetparte que estava me matando. Funcionou por anos e, de repente, parou de funcionar e começou a reclamar de um blá blá estático / não estático. Enfim, obrigado!
Eli Gassert
-1

mudança

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

Para

// This next line works!
Expression<Func<T>> DangerousExpression = () => dangerousCall();
mheyman
fonte
Servir, é uma forma absolutamente legal de obter uma expressão. sintaxe sugar para construí-lo por meio de expression.lambda e expression.call. Por que você acha que deveria falhar em tempo de execução?
Roman Pokrovskij