83

我是这个场景中的假人。

我试图在谷歌上阅读这些是什么,但我就是不明白。有人可以简单解释一下它们是什么以及它们为什么有用吗?

编辑:我说的是 .Net 中的 LINQ 功能。

4

7 回答 7

91

我读过的关于表达式树的最好解释是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.

在这里,我们看到我们可以从表达式中获得大量信息。

但是我们为什么需要它呢?

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

LINQ to SQL 查询不会在您的 C# 程序中执行。相反,它被翻译成 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 服务器数据库。显然,将诸如表达式树之类的数据结构翻译成 SQL 要比将原始 IL 或可执行代码翻译成 SQL 要容易得多。为了稍微夸大问题的难度,想象一下尝试将一系列 0 和 1 转换为 SQL!

当需要将您的查询表达式转换为 SQL 时,表示您的查询的表达式树被拆开并分析,就像我们在上一节中拆开简单的 lambda 表达式树一样。当然,解析 LINQ to SQL 表达式树的算法比我们使用的要复杂得多,但原理是一样的。一旦分析了表达式树的各个部分,LINQ 就会仔细考虑它们并决定编写返回请求数据的 SQL 语句的最佳方式。

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

因为查询被封装在这样一个抽象数据结构中的编译器,所以编译器可以自由地以几乎任何它想要的方式解释它。它不会强制以特定顺序或特定方式执行查询。相反,它可以分析表达式树,发现你想要做什么,然后决定如何去做。至少在理论上,它可以自由考虑任何数量的因素,例如当前的网络流量、数据库的负载、可用的当前结果集等。实际上 LINQ to SQL 并没有考虑所有这些因素,但理论上它几乎可以自由地做它想做的事。此外,可以将此表达式树传递给您手动编写的一些自定义代码,这些代码可以对其进行分析并将其转换为与 LINQ to SQL 生成的非常不同的东西。

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

于 2013-12-24T20:00:11.210 回答
42

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

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


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

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

这段代码使编译器生成一个赋值,就是这样。在大多数情况下,这就是您所关心的。

使用常规代码,您的应用程序无法追溯并查看hypotenuse以确定它是通过执行Math.Sqrt()调用生成的;此信息根本不包括在内。

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

Func<int, int, double> 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)

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

于 2009-03-08T11:26:04.393 回答
16

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

a + b*2

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

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

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

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

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

于 2009-03-08T11:25:40.477 回答
16

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

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

基本上有两种方法可以做到这一点:

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

后者是我们称为“编译器”的所有程序所做的事情。

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

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

于 2009-06-07T08:25:49.200 回答
7

IIUC,表达式树类似于抽象语法树,但表达式通常产生单个值,而 AST 可以表示整个程序(包含类、包、函数、语句等)

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

    *
   / \ 
  +   5
 / \
2   3

递归地(自下而上)对每个节点求值,得到根节点处的值,即表达式的值。

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

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

于 2009-03-08T11:21:13.267 回答
5

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

更多关于 DLR 的信息

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

回到我的第一句话,表达式树是对 C# 的补充,它开启了使用 DLR 的能力。在此之前,C# 是一种更加静态的语言——所有变量类型都必须声明为特定类型,并且所有代码都必须在编译时编写。

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

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

使用表达式树,您现在可以动态更改程序中的代码并执行搜索。具体来说,您可以通过 LINQ 执行此操作。

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

超越数据
表达式树的主要用途是管理数据。但是,它们也可以用于动态生成的代码。因此,如果您想要一个动态定义的函数(ala Javascript),您可以创建一个表达式树,对其进行编译并评估结果。

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

表达式树作为编译器

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

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

于 2013-08-06T12:57:22.873 回答
-3

您所指的表达式树是表达式评估树吗?

如果是,那么它是由解析器构造的树。Parser 使用 Lexer/Tokenizer 从程序中识别令牌。Parser 从标记构造二叉树。

这里是详细解释

于 2009-03-08T11:18:18.607 回答