LINQ to Objects系列(4)表达式树

时间:2021-01-24 18:44:03

  为了进一步加深对Lambda表达式的理解,我们需要掌握一个新的知识,Lambda表达式树,可能听名字看起来很高深和难以理解,但实际上理解起来并没有想象中那么难,这篇文章我想分以下几点进行总结。

1,表达式树的语法

2,将代码转换到数据

3,探索表达式树

4,将数据转换到代码

5,IQueryable<T>和表达式树

6,为什么要将LINQ to SQL查询表达式转换成表达式树?

7,IQueryable<T>和IEnumerable<T>

8,总结

表达式树的语法

//利用Lambda表达式定义一个Func委托
Func<int, int, int> function = (a, b) => a + b;

变量function指向两个数字相加的原生可执行的代码,上面这个Lambda表达式等价于下面这个方法。

public int function(int a, int b)
{
    return a + b;
}

这个Lambda表达式和这个方法都可以这样调用。

int c = function(1,2); //结果为:3

将代码转换到数据

表达式树不是一段可执行代码,而是一种数据结构。那么怎么将Lambda表达式转换成表达式树呢?

我们可以使用命名空间System.Linq.Expressions下的Expression类来实现这个需求。

例如我们先创建一个表达式树,如下代码。

//创建表达式树
Expression<Func<int, int, int>> expression = (a, b) => a + b;

这样,我们就创建一个类型为Expression<T>的表达式树,标识expression不是可执行代码;它是一个名叫表达式树的数据结构。我们可以使用工具ExpressionTreeVisualizer来浏览表达式树,如下图。

LINQ to Objects系列(4)表达式树

探索表达式树

Expression<TDelegate>类有四个属性:

  • Body: 得到表达式的主体。
  • Parameters: 得到lambda表达式的参数.
  • NodeType: 获取树的节点的ExpressionType。共45种不同值,包含所有表达式节点各种可能的类型,例如返回常量,例如返回参数,例如取两个值的小值(<),例如取两个值的大值(>),例如将值相加(+),等等。
  • Type: 获取表达式的一个静态类型。在这个例子里,表达式的类型是Func<intintint>。

那么怎么查看表达式树中的参数名称呢?从上图中我们可以看出,参数是一个ReadOnlyCollection集合,所以我们可以通过索引来访问。如下代码。

//访问表达式树的参数
Console.WriteLine("参数1:{0},参数2:{1}",expression.Parameters[0],expression.Parameters[1]);

接下来,怎么查看表达式树的Body体呢?在这个例子里是(a+b)。代码如下。

//访问表达式树的Body
BinaryExpression body = expression.Body as BinaryExpression;
ParameterExpression left = body.Left as ParameterExpression;
ParameterExpression right = body.Right as ParameterExpression;
Console.WriteLine(expression.Body);
Console.WriteLine(" 表达式左边部分: " + "{0}{4} 节点类型: {1}{4} 表达式右边部分: {2}{4} 类型: {3}{4}", left.Name, body.NodeType, right.Name, body.Type, Environment.NewLine);

输出结果为:

LINQ to Objects系列(4)表达式树

通过探索表达式树,我们可以分析表达式的各个部分发现它的组成。你可以看见,我们的表达式的所有元素都展示为像节点这样的数据结构。表达式树是代码转换成的数据。

将数据转换到代码

我们可以将代码转换为数据,那么我们也可将数据转换为代码。下面的代码说明了如果将数据(表达式树数据结构)转换为代码。

//将数据(表达式树)转换为代码
int result = expression.Compile()(1,2);
Console.WriteLine(result); //输出结果为:3

可以发现,程序输出结果与Lambda表达式执行结果一样。

 IQueryable<T>和表达式树

现在至少你有一个抽象的概念理解表达式树,现在是时候回来理解其在LINQ中的关键作用了,尤其是在LINQ to SQL中。花点时间考虑这个标准的LINQ to SQL查询表达式:

var query = from c in db.Customers 
            where c.City == "Nantes" 
            select new { c.City, c.CompanyName };


你可能知道,这里LINQ表达式返回的变量query是IQueryable类型。这里是IQueryable类型的定义:

public interface IQueryable : IEnumerable 
{
  Type ElementType { get; }
  Expression Expression { get; }
  IQueryProvider Provider { get; }
}

你可以看见,IQueryable包含一个类型为Expression的属性,Expression是Expression<T>的基类。IQueryable的实例被设计成拥有一个相关的表达式树。它是一个等同于查询表达式中的可执行代码的数据结构。

 为什么要将LINQ to SQL查询表达式转换成表达式树?

现在我们知道,表达式树是一个用来表示可执行代码的数据结构。那我们为什么要将LINQ to SQL查询表达式转换成表达式树呢?

一个LINQ to SQL查询不是在C#程序里执行的,而是被转换成SQL语句,通过网络发送,最后在数据库服务器上执行的。也就是说,下面这个LINQ查询不是在C#程序里执行的。

var query = from c in db.Customers

            where c.City == "Nantes"

            select new { c.City, c.CompanyName };

它是被转换成SQL语句后在数据库服务器上运行的。转换后的SQL语句如下代码。

SELECT [t0].[City], [t0].[CompanyName]
FROM [dbo].[Customers] AS [t0] 
WHERE [t0].[City] = @p0

现在也许可以回答上面的问题。可以用一句话总结:表达式树是为了更方面地将查询表达式转换成字符串(这里指的是SQL语句)并交给其它程序(这里一般指数据库服务器)执行。

IQueryable<T>和IEnumerable<T>

 我们知道,LINQ to Objects通常返回IEnumerable<T>,而LINQ to SQL返回的是IQueryable<T>。那为什么它们返回的类型会不一样呢?

我们先来看它们的定义,也许我们可以从它们的定义中找到问题的答案。

IEnumberable<T>的定义如下:

public interface IEnumerable<T> : IEnumerable 
{ 
   IEnumerator<T> GetEnumerator();
}

IQueryable<T>的定义如下:

public interface IQueryable : IEnumerable 
{
  Type ElementType { get; }
  Expression Expression { get; } //表达式树
  IQueryProvider Provider { get; }
}

可以看出,IQueryable<T>包含一个Expression表达式树的定义而IEnumberable<T>却没有,这同时也揭示了一个现象,表达式树通常用在LINQ to SQL查询中,而LINQ to Objects中却很少使用。

那为什么LINQ to Objects中很少使用表达式树呢?是因为LINQ to Objects查询通常在.net程序中就可以完成,不需要将其转换成字符串(或SQL语句)发送到其它程序中执行。

那么针对这两种返回类型,我们该怎么选择呢?这里有两条原则可以参考:

  • 如果查询表达式可以在本程序里执行的,那么使用Enumberable<T>就可以完成任务。
  • 如果查询表达式需要被转换成字符串并发送到其它程序中执行的,那么就应该使用IQueryable<T>和表达式树。

总结

通过以上内容的学习,发现表达式树并没有想象中那么难以理解,关于表达式树我想用以一几句通俗易懂的话总结。

  1. 表达式树是一种用来表示可执行代码(一般指Lambda查询表达式)的树形数据结构。
  2. 表达式树在LINQ to SQL中使用得非常多(因为LINQ to SQL查询返回IQueryable<T>类型),通过表达式树,LINQ to SQL查询表达式更方便地被解析成SQL语句并发送到数据库服务器上执行。
  3. 一条最佳实践原则:LINQ查询如果在程序内执行的不需要表达式树,当代码在程序外部执行时则需要使用表达式树。