将一个 .net Func<T> 转换成一个 .net Expression<Func<T>>

142

使用方法调用很容易从lambda转换为Expression...

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

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

但我希望将这个 Func 转化为表达式,只有在极少数情况下需要这样做...

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

不能隐式地将类型'System.Func<T>'转换为'System.Linq.Expressions.Expression<System.Func<T>>',这行代码无法编译通过。显式转换也无法解决这个问题。我是否忽略了某个可以解决这个问题的方法?


我并不认为“罕见情况”示例有太多用处。调用者正在传递Func<T>。没有必要通过异常将该Func<T>重复返回给调用者。 - Adam Ralph
2
调用方未处理异常。而且,由于有多个调用站点传递不同的Func<T>,在调用方捕获异常会产生重复。 - Dave Cameron
1
异常堆栈跟踪旨在显示此信息。如果在调用Func<T>时抛出异常,这将显示在堆栈跟踪中。顺便说一下,如果您选择走另一条路,即接受表达式并编译它以进行调用,则会失去这个功能,因为堆栈跟踪将显示类似于“at lambda_method(Closure)”的内容,用于调用已编译的委托。 - Adam Ralph
我猜你应该看一下这个链接中的答案。 - Ibrahim Kais Ibrahim
9个回答

115
哦,这并不容易。 Func<T> 表示一个泛型的 delegate,而不是一个表达式。如果有办法获取到原始表达式的话(由于编译器进行了优化和其他操作,因此可能会丢失一些数据,所以可能无法获取到原始表达式),那么唯一的方法就是在运行时动态反汇编IL,并推断出表达式(这绝对不容易)。将 lambda 表达式视为数据 (Expression<Func<T>>) 是由编译器完成的魔术操作(基本上,编译器在代码中构建了一个表达式树,而不是将其编译成 IL)。

相关事实

这就是为什么像 Lisp 这样极力推崇 lambda 的语言通常更容易作为解释器实现的原因。在这些语言中,代码和数据本质上是相同的东西(甚至在运行时也是如此),但我们的芯片无法理解这种代码形式,因此我们必须通过构建一个能够理解它的解释器来模拟这样的机器(Lisp 等语言所选择的方式),或者在某种程度上牺牲一定的功能(代码将不再完全等同于数据)(C# 所做出的选择)。在 C# 中,编译器通过允许 lambda 被解释为Func<T>Expression<Func<T>> 这两种形式,在编译时产生了代码作为数据的错觉。


4
Lisp 不一定需要解释,它可以很容易地编译。宏需要在编译时展开,如果您想支持 eval,则需要启动编译器,但除此之外,在进行这方面的工作时没有任何问题。 - configurator
4
"Expression<Func<T>> DangerousExpression = () => dangerousCall();" 这句话不容易吗? - mheyman
13
这将创建一个关于封装操作的新“表达式”,但它不会包含有关“dangerousCall”委托内部的表达式树信息。 - Nenad

50
    private static Expression<Func<T, bool>> FuncToExpression<T>(Func<T, bool> f)  
    {  
        return x => f(x);  
    } 

1
我想遍历返回表达式的语法树。这种方法是否可以让我做到这一点? - Dave Cameron
12
不行,详见以上答案 - 已经编译的 'Func' 将隐藏在一个新的表达式中。这只是在代码上添加了一层数据;你可能需要遍历一层才能找到参数 'f',但没有更多的细节信息,所以你仍然在原地。 - Jonno
它会导致Entity Framework Core的客户端评估异常。 - XAMT
@XAMT,你解决了这个异常吗?提前感谢! - Luca
@Luca,您应该将语句更改为EF可理解的内容。(例如,将 .Equal(< ignore casing parameter >) 更改为 == x.ToLower() - XAMT

26

你应该考虑改变这个方法的方式。传入一个表达式,然后进行编译和运行。如果失败了,你已经有了需要检查的表达式。

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

显然,你需要考虑这种方法的性能影响,并确定它是否是你真正需要做的事情。


11

NJection.LambdaConverter 是一个将委托转换为表达式的库

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

11
如果你有时需要表达式并有时需要委托,你有两个选择:
  • 拥有不同的方法(每个方法都是为了实现特定的功能)
  • 始终接受 Expression<...> 版本,如果你需要委托,只需使用 .Compile().Invoke(...) 进行调用。显然这会有成本。

8

您可以通过 .Compile() 方法来实现另一种方式 - 不确定这对您是否有用:

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

7
 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
1
顺便说一句,这可能不是主要问题的解决方案,但却是我所需要的。call.Target 部分让我苦恼了很久。它已经运行了多年,然后突然停止工作,并开始抱怨静态/非静态等等。无论如何,谢谢! - Eli Gassert
我已经使用Override的答案多年了,但我的表达式现在包含一个Span<char>,这将无法工作。这本质上是相同的东西,但可以使用Span<char> - Josh Close

3

-1

更改

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

// This next line works!
Expression<Func<T>> DangerousExpression = () => dangerousCall();

Servy,它是一种完全合法的获取表达式的方式。可以通过 expression.lambda 和 expression.call 来构建它。你为什么认为它会在运行时失败? - Roman Pokrovskij

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接