你为什么要用表达功能>而不是Func<T>?

我理解lambdas和FuncAction委托。但表情我头疼不已。< / p >

在什么情况下你会使用Expression<Func<T>>而不是普通的Func<T>?

280184 次浏览

当您希望将lambda表达式视为表达式树并查看其内部而不是执行它们时。例如,LINQ to SQL获取表达式并将其转换为等效的SQL语句并将其提交给服务器(而不是执行lambda)。

从概念上讲,Expression<Func<T>>Func<T>完全不同的Func<T>表示delegate,它基本上是一个指向方法的指针,Expression<Func<T>>表示lambda表达式的树形数据结构。这个树结构描述lambda表达式的作用,而不是做实际的事情。它基本上保存了关于表达式、变量、方法调用等组合的数据。(例如,它包含这样的信息:this lambda is some constant + some parameter)。您可以使用此描述将其转换为实际的方法(使用Expression.Compile)或使用它执行其他操作(如LINQ to SQL示例)。将lambdas视为匿名方法和表达式树的行为纯粹是编译时的事情。

Func<int> myFunc = () => 10; // similar to: int myAnonMethod() { return 10; }

将有效地编译为一个IL方法,该方法得不到任何结果并返回10。

Expression<Func<int>> myExpression = () => 10;

将被转换为描述不获取参数并返回值10的表达式的数据结构:

Expression vs Func 大图

虽然它们在编译时看起来是一样的,但编译器生成的是完全不同的

当您希望将函数视为数据而不是代码时,可以使用表达式。如果您想操作代码(作为数据),可以这样做。大多数情况下,如果你认为不需要表达式,那么你可能就不需要使用表达式。

LINQ是典型的例子(例如,与数据库对话),但事实上,任何时候您更关心表达什么要做的事情,而不是实际去做它。例如,我在RPC堆栈protobuf-net中使用这种方法(以避免代码生成等)-所以你调用一个方法:

string result = client.Invoke(svc => svc.SomeMethod(arg1, arg2, ...));

这将分解表达式树以解析SomeMethod(以及每个参数的值),执行RPC调用,更新任何ref/out参数,并从远程调用返回结果。这只能通过表达式树实现。我要讲的是第三点。

另一个例子是为了编译为lambda而手动构建表达式树,正如通用的运营商代码所做的那样。

在选择表达式和Func时,一个非常重要的考虑因素是IQueryable提供者,如LINQ to Entities可以“消化”你在表达式中传递的内容,但会忽略你在Func中传递的内容。关于这个主题,我有两篇博文:

< p > # 0爱上LINQ -第7部分:表达式和功能函数(最后一部分)

我想补充一些关于Func<T>Expression<Func<T>>之间差异的注释:

  • Func<T>只是一个普通的老式MulticastDelegate;
  • Expression<Func<T>>是lambda表达式以表达式树的形式表示;
  • 表达式树可以通过lambda表达式语法或API语法构造;
  • 表达式树可以编译为委托Func<T>;
  • 逆转换在理论上是可行的,但这是一种反编译,没有内置功能,因为这不是一个简单的过程;
  • 表达式树可以通过ExpressionVisitor;
  • IEnumerable的扩展方法操作Func<T>;
  • IQueryable扩展方法使用Expression<Func<T>>操作。
有一篇文章用代码示例描述了细节:
# 0。< / p >

希望对大家有所帮助。

主要原因是当您不想直接运行代码,而是想检查它时。这可能有很多原因:

  • 将代码映射到不同的环境(例如。c#代码到实体框架中的SQL
  • 在运行时替换部分代码(动态编程或甚至普通的DRY技术)
  • 代码验证(在模拟脚本或进行分析时非常有用)
  • 序列化——表达式可以相当容易和安全地序列化,而委托不能
  • 在本质上不是强类型的事物上的强类型安全性,并且即使在运行时进行动态调用也利用编译器检查(ASP。NET MVC 5与Razor是一个很好的例子)

我添加了一个针对新手的答案,因为这些答案似乎超出了我的理解范围,直到我意识到它是多么简单。有时是你认为事情很复杂,这让你无法“理清思路”。

我不需要理解其中的区别,直到我遇到了一个非常烦人的“bug”,试图一般地使用LINQ-to-SQL:

public IEnumerable<T> Get(Func<T, bool> conditionLambda){using(var db = new DbContext()){return db.Set<T>.Where(conditionLambda);}}

这工作得很好,直到我开始在更大的数据集上得到outofmemoryexception。在lambda中设置断点使我意识到它正在逐一遍历表中的每一行,寻找与lambda条件匹配的条件。这让我困惑了一段时间,因为为什么它把我的数据表作为一个巨大的IEnumerable,而不是像它应该做的那样做LINQ-to-SQL ?它也在我的LINQ-to-MongoDb对应版本中做同样的事情。

解决办法很简单,就是把Func<T, bool>变成Expression<Func<T, bool>>,所以我谷歌了一下为什么它需要Expression而不是Func,结果在这里。

所以a => a + 1变成了“左边有一个int a”。右边加1 "就是这样。你可以回家了。它显然比这更有结构,但这本质上就是表达式树的全部——没有什么可以让你理解的。

理解了这一点,就很清楚为什么LINQ-to-SQL需要Expression,而Func是不够的。Func并没有带着一种方法进入它自己,看到如何将它翻译成SQL/MongoDb/其他查询的本质。你看不出它是在做加法、乘法还是减法。你能做的就是运行它。Expression,另一方面,允许你查看委托内部并看到它想要做的所有事情。这使您能够将委托转换为您想要的任何内容,例如SQL查询。Func没有工作,因为我的DbContext对lambda表达式的内容是盲目的。因此,它不能将lambda表达式转换为SQL;然而,它做了退而求其次的事情,在我的表中的每一行中迭代这个条件。

编辑:应约翰·彼得的要求,对我的最后一句话进行解释:

IQueryable扩展了IEnumerable,所以IEnumerable的方法,如Where(),获得了接受Expression的重载。当你传递Expression给它时,你会保留一个IQueryable作为结果,但当你传递Func时,你会退回到IEnumerable基数上,你会得到一个IEnumerable作为结果。换句话说,你没有注意到你已经把你的数据集变成了一个要迭代的列表,而不是要查询的东西。除非你仔细观察这些签名,否则很难发现区别。

Krzysztof Cwalina的书中对此有一个更哲学的解释(框架设计指南:可重用。net库的约定、习惯用语和模式);

Rico Mariani

编辑非图像版本:

大多数情况下,如果需要运行一些代码,则需要FuncAction。当代码在运行之前需要分析、序列化或优化时,您需要ExpressionExpression用于思考代码,Func/Action用于运行它。

当使用LINQ-to-SQL时,将Func<>传递给Where()Count()是不好的。真正的坏。如果你使用Func<>,那么它会调用IEnumerable LINQ的东西,而不是IQueryable,这意味着整个表被拉进来,然后被过滤。Expression<Func<>>的速度要快得多,因为它执行了在SQL server上的过滤—特别是当您正在查询位于另一个服务器上的数据库时。

这里过于简化了,但Func是一个机器,而Expression是一个蓝图。: D

很高兴知道您可以将Func<TEntity, bool>Expression<Func<TEntity, bool>>这样的AsQueryable()扩展方法一起使用。

Func<App, bool> filter = x => x.Alias.Contains("gan");var query = dbSet.Where(filter).AsQueryable();

直到使用Count()ToList()这样的执行方法,查询才会执行。

这是我的两分钱…

  1. Func<T> =刚被执行的委托/方法,仅此而已。
  2. Expression<Func<T>> =它被转换为另一种形式。例如,LINQ to Entities表达式被转换为等效的SQL查询。

想象一下,两者看起来很相似,但表达式就像一个数据结构,具有反射的能力。编译器实际上知道关于它的签名和主体的所有信息(类似于类反射)。利用这些知识,表达式被转换为其他形式,就像LINQ被转换为SQL查询一样。

现在,还有另一个角度来处理IQueryable行为。建议您始终将LINQ的Expression方法传递给WhereCount方法,以便您的查询过滤器在SQL服务器上运行,而不是在内存中提取数据然后进行过滤。