表达树的假人?[关闭]


83

在这种情况下,我是假人。

我试图在Google上阅读这些内容,但我不明白。有人可以简单说明一下它们是什么以及为什么有用吗?

编辑:我正在谈论.Net中的LINQ功能。


1
我知道这篇文章比较老,但是最近我一直在研究Expression Trees。开始使用Fluent NHibernate之后,我变得很感兴趣。詹姆斯·格雷戈里(James Gregory)广泛使用所谓的静态反射,他有一个介绍:jagregory.com/writings/introduction-to-static-reflection 要查看实际的静态反射和表达式树,请查看Fluent NHibernate源代码(fluentnhibernate.org)。它非常干净,并且是一个非常酷的概念。
Jim Schubert

Answers:


88

我曾经读过的关于表达式树的最佳解释是Charlie Calvert的这篇文章

把它们加起来;

表达式树代表什么,你想做的事,而不是如何你想这样做。

考虑以下非常简单的lambda表达式:
Func<int, int, int> function = (a, b) => a + b;

该语句包括三个部分:

  • 声明: Func<int, int, int> function
  • 等于运算符: =
  • Lambda表达式: (a, b) => a + b;

该变量function指向知道如何将两个数字相加的原始可执行代码。

这是委托和表达式之间最重要的区别。您调用function(a Func<int, int, int>)却不知道它将如何处理您传递的两个整数。它需要两个并返回一个,这是您的代码所能知道的。

在上一节中,您了解了如何声明一个指向原始可执行代码的变量。表达式树不是可执行代码,它们是数据结构的一种形式。

现在,与委托不同,您的代码可以知道表达式树的作用。

LINQ提供了一种简单的语法,用于将代码转换为称为表达式树的数据结构。第一步是添加一个using语句以引入Linq.Expressions名称空间:

using System.Linq.Expressions;

现在我们可以创建一个表达式树:
Expression<Func<int, int, int>> expression = (a, b) => a + b;

上一个示例中显示的相同lambda表达式将转换为声明为type的表达式树Expression<T>。标识符expression 不是可执行代码;它是一种称为表达式树的数据结构。

这意味着您不能像调用委托那样仅调用表达式树,而是可以对其进行分析。那么,通过分析变量,您的代码可以理解什么expression呢?

// `expression.NodeType` returns NodeType.Lambda.
// `expression.Type` returns Func<int, int, int>.
// `expression.ReturnType` returns Int32.

var body = expression.Body;
// `body.NodeType` returns ExpressionType.Add.
// `body.Type` returns System.Int32.

var parameters = expression.Parameters;
// `parameters.Count` returns 2.

var firstParam = parameters[0];
// `firstParam.Name` returns "a".
// `firstParam.Type` returns System.Int32.

var secondParam = parameters[1].
// `secondParam.Name` returns "b".
// `secondParam.Type` returns System.Int32.

在这里,我们可以从表达式中获得很多信息。

但是为什么我们需要那个呢?

您已经了解到,表达式树是代表可执行代码的数据结构。但是到目前为止,我们还没有回答为什么要进行这种转换的中心问题。这是我们在本文开头提出的问题,现在是时候回答它。

在C#程序中未执行LINQ to SQL查询。而是将其转换为SQL,通过电线发送,并在数据库服务器上执行。换句话说,以下代码永远不会在程序内部实际执行:
var query = from c in db.Customers where c.City == "Nantes" select new { c.City, c.CompanyName };

它首先被转换为以下SQL语句,然后在服务器上执行:
SELECT [t0].[City], [t0].[CompanyName] FROM [dbo].[Customers] AS [t0] WHERE [t0].[City] = @p0

在查询表达式中找到的代码必须转换为SQL查询,该查询可以作为字符串发送到另一个进程。在这种情况下,该过程恰好是SQL Server数据库。与将原始IL或可执行代码转换为SQL相比,将表达式树之类的数据结构转换为SQL显然要容易得多。为了稍微夸大问题的难度,请想象一下尝试将一系列零和一转换为SQL!

当需要将查询表达式转换为SQL时,将分解并分析代表查询的表达式树,就像我们在上一节中分解了简单的lambda表达式树一样。当然,用于将LINQ转换为SQL表达式树的算法比我们使用的算法复杂得多,但是原理是相同的。一旦分析了表达式树的各个部分,LINQ便会对其进行仔细考虑,并确定编写可返回请求数据的SQL语句的最佳方法。

创建表达式树是为了完成将查询表达式之类的代码转换为字符串的任务,该字符串可以传递给其他进程并在那里执行。就这么简单。这里没有什么大谜,也不需要挥舞魔杖。一个人简单地获取代码,将其转换为数据,然后分析数据以查找组成部分,这些部分将被转换为可传递给另一个进程的字符串。

因为查询来自封装在这种抽象数据结构中的编译器,所以编译器可以自由地以其所需的任何方式对其进行解释。不必强制以特定顺序或特定方式执行查询。相反,它可以分析表达式树,发现您想要做的事情,然后决定如何做。至少从理论上讲,它可以自由考虑任何因素,例如当前的网络流量,数据库的负载,它具有的当前结果集等。实际上,LINQ to SQL并未考虑所有这些因素。 ,但从理论上讲,它几乎可以免费执行所需的操作。此外,可以将此表达式树传递给您手工编写的一些自定义代码,这些代码可以对其进行分析并将其转换为与LINQ to SQL所生成的内容完全不同的东西。

再一次,我们看到表达式树允许我们代表(表达什么?)什么,我们想做的事情。我们使用翻译器来决定如何使用表达式。


2
更好的答案之一。
约翰尼

4
极好的答案。可以给这个精妙的解释增加一个小方面的东西是-表达式树的另一种用法是,您可以在运行时动态修改表达式树,然后将其提供给执行,这有时是非常有用的。
Yan D

41

表达式树是一种将可执行​​代码转换为数据的机制。使用表达式树,可以产生一个表示程序的数据结构。

在C#中,您可以使用Expression<T>该类使用由lambda表达式生成的表达式树。


在传统程序中,您应编写如下代码:

double hypotenuse = Math.Sqrt(a*a + b*b);

该代码使编译器生成一个分配,仅此而已。在大多数情况下,这就是您所关心的。

使用常规代码,您的应用程序将无法追溯回溯并hypotenuse确定它是通过执行Math.Sqrt()调用产生的。此信息根本不属于所包含的内容。

现在,考虑一个如下的lambda表达式:

Func<int, int, int> hypotenuse = (a, b) => Math.Sqrt(a*a + b*b);

这与以前有些不同。现在hypotenuse实际上是对可执行代码块的引用。如果你打电话

hypotenuse(3, 4);

您将获得5返回的值。

我们可以使用表达式树来探索生成的可执行代码块。尝试以下方法:

Expression<Func<int, int, int>> addTwoNumbersExpression = (x, y) => x + y;
BinaryExpression body = (BinaryExpression) addTwoNumbersExpression.Body;
Console.WriteLine(body);

这将产生:

(x + y)

使用表达式树可以使用更高级的技术和操作。


7
好吧,直到最后我一直都和你在一起,但我仍然不明白为什么这很重要。我很难考虑应用程序。

1
他只是在用一个简化的例子。真正的强大之处在于,您可以探索表达式树的代码也可以负责解释它,并将语义应用于表达式。
Pierreten

2
是的,如果他/她解释了为什么(x + y)实际上对我们有用,那么这个答案会更好。我们为什么要探索(x + y),我们该怎么做?
Paul Matthews 2014年

您无需进行探索,只需查看您的查询以及在这种情况下将转换为SQL的其他语言即可
stanimirsp

15

表达式树是表达式的内存表示形式,例如算术或布尔表达式。例如,考虑算术表达式

a + b*2

由于*具有比+高的运算符优先级,因此表达式树的构建如下:

    [+]
  /    \
 a     [*]
      /   \
     b     2

有了这个树,就可以对a和b的任何值进行评估。另外,您可以将其转换为其他表达式树,例如以导出表达式。

当您实现表达式树时,我建议创建一个基类 Expression。因此,类BinaryExpression将用于所有二进制表达式,例如+和*。然后,您可以引入VariableReferenceExpression来引用变量(例如a和b),以及另一个类ConstantExpression(对于示例中的2)。

在许多情况下,表达式树是作为解析输入(直接来自用户或来自文件)的结果而构建的。为了评估表达式树,我建议使用Visitor模式


15

简短的回答:能够编写相同类型的LINQ查询并将其指向任何数据源,这是很好的。没有它,您将无法获得“语言集成”查询。

长答案:您可能知道,在编译源代码时,您正在将其从一种语言转换为另一种语言。通常从高级语言(C#)到较低级别的(IL)。

基本上有两种方法可以执行此操作:

  1. 您可以使用查找和替换来翻译代码
  2. 您解析代码并获得解析树。

后者是我们称为“编译器”的所有程序的功能。

一旦有了解析树,您就可以轻松地将其翻译成任何其他语言,这就是表达式树所允许的。由于代码是作为数据存储的,因此您可以执行任何您想做的事情,但可能只是要将其翻译成其他语言。

现在,在LINQ to SQL中,表达式树变成了SQL命令,然后通过电线发送到数据库服务器。据我所知,他们在翻译代码时并没有做任何真正的尝试,但他们可以。例如,查询提供者可以根据网络条件创建不同的SQL代码。


6

IIUC,一个表达式树类似于抽象语法树,但表达式通常只包含一个值,而AST可以表示整个程序(带有类,包,函数,语句等)。

无论如何,对于表达式(2 + 3)* 5,树是:

    *
   / \ 
  +   5
 / \
2   3

递归评估每个节点(自下而上)以获取根节点处的值,即表达式的值。

当然,您也可以使用一元运算符(否定)或三元运算符(if-then-else),如果表达式语言允许,则可以使用函数(n元,即任意数量的操作数)。

在类似的树上进行类型评估和类型控制。


5

DLR
表达式树是C#的补充,以支持动态语言运行时(DLR)。DLR也是负责为我们提供声明变量的“ var”方法的原因。(var objA = new Tree();

有关DLR的更多信息

本质上,Microsoft希望为动态语言(例如LISP,SmallTalk,Javascript等)开放CLR。为此,他们需要能够即时分析和评估表达式。在DLR出现之前,这是不可能的。

回到我的第一句话,表达式树是C#的新增功能,它可以使用DLR。在此之前,C#是一种更加静态的语言-所有变量类型都必须声明为特定类型,并且所有代码都必须在编译时编写。

将其与数据
表达式树一起使用可打开动态代码的闸门。

举例来说,假设您正在创建一个房地产网站。在设计阶段,您知道可以应用的所有过滤器。要实现此代码,您有两种选择:可以编写一个循环,将每个数据点与一系列If-Then检查进行比较;或者您可以尝试使用动态语言(SQL)构建查询,然后将其传递给可以为您执行搜索的程序(数据库)。

使用表达式树,您现在可以随时更改程序中的代码并执行搜索。具体来说,您可以通过LINQ做到这一点。

(请参阅更多:MSDN:如何:使用表达式树来构建动态查询)。

数据之外
表达式树的主要用途是管理数据。但是,它们也可以用于动态生成的代码。因此,如果您想要一个动态定义的功能(例如Java语言),则可以创建一个Expression Tree,对其进行编译并评估结果。

我会更深入一点,但是这个站点做得更好:

表达式树作为编译器

列出的示例包括为变量类型创建通用运算符,手动滚动lambda表达式,高性能浅层克隆以及将读取/写入属性从一个对象动态复制到另一个对象。

摘要
表达式树是在运行时编译和评估的代码的表示。它们允许动态类型,这对于数据操作和动态编程很有用。


是的,我知道我迟到了,但是我想写这个答案作为自己理解它的一种方式。(此问题在我的互联网搜索中排名很高。)
理查德(Richard

不错的工作。这是一个很好的答案。
Rich Bryant

5
“ var”关键字与DLR无关。您将其与“动态”混淆了。
Yarik

这是对var的一个很好的小答案,表明Yarik是正确的。不过,感谢您提供其余的答案。 quora.com/…–
约翰尼,

1
都错了 var是一个编译时语法糖-它与表达式树,DLR或运行时无关。var i = 0就像您编写一样被编译int i = 0,因此您不能var用来表示在编译时未知的类型。表达式树不是“支持DLR的补充”,它们是在.NET 3.5中引入的,以允许LINQ。另一方面,.NET 4.0中引入了DLR,以允许使用动态语言(如IronRuby)和dynamic关键字。表达式树实际上是DLR用来提供互操作的,并非相反。
Şafak古尔

-3

您引用的表达式树是“表达式评估树”吗?

如果是,则为解析器构造的树。解析器使用Lexer / Tokenizer从程序中识别令牌。解析器根据令牌构造二叉树。

是详细的说明


好吧,尽管OP所引用的表达式树的工作原理与解析树相同,并且具有相同的底层概念,但它是在运行时通过代码动态完成的,但是请注意,罗斯林编译器的介绍如果没有完全消除,两者之间的划分将变得非常模糊。
yoel halb '02
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.