LINQ查漏补缺之Expression与ExpressionTree

Expression与ExpressionTree

LINQ 表达式(Expression)

可以将lambda表达式分配给Func或Action类型委托,以处理内存中的集合。.NET编译器在编译时将分配给Func或Action类型委托的lambda表达式转换为可执行代码。

LINQ引入了一种名为Expression的新类型,该类型代表强类型的lambda表达式。这意味着lambda表达式也可以分配给Expression 类型。.NET编译器将分配给Expression 的lambda表达式转换为Expression树,而不是可执行代码。远程LINQ查询提供程序使用此表达式树作为数据结构,以其构建运行时查询(例如LINQ-to-SQL,EntityFramework或实现IQueryable 接口的任何其他LINQ查询提供程序)。

lambda表达式分配给Func或Action委托与LINQ中的Expression时的区别:

Fuc = lambda expression -> Compliles into -> Excutable code

Expression = lambda expression -> Compliees into -> Expression Tree -> Used as a data structure by LINQ Provider

定义 Expression

引用System.Linq.Expressions命名空间,并使用Expression 类定义一个Expression。Expression 需要委托类型Func或Action。

例如,你可以将 lambda 表达式赋给 Func 类型委托 的 isTeenAger 变量,如下所示:

示例:在C#中为表达式定义Func委托

public class Student
{
public int StudentID { get; set; }
public string StudentName { get; set; }
public int Age { get; set; }
}
Func<Student, bool> isTeenAger = s => s.Age > 12 && s.Age < 20;

现在,您可以使用Expresson包装Func委托,将上述Func类型委托转换为Expression,如下所示:

示例:在C#中定义表达式 Expresson

Expression<Func<Student, bool>> isTeenAgerExpr = s => s.Age > 12 && s.Age < 20;

以相同的方式,如果您不从委托返回值,则还可以用Expression包装Action 类型委托。

示例:在C#中定义表达式

Expression<Action<Student>> printStudentName = s => Console.WriteLine(s.StudentName);

因此,您可以定义Expression 类型。现在,让我们看看如何调用由Expression 包装的委托。

调用表达式(Expression)

您可以用与委托相同的方式调用由Expression包裹的委托,但是首先需要使用Compile()方法进行编译。Compile()返回FuncAction类型的委托,以便您可以像委托一样调用它。

示例:在C#中调用表达式

Expression<Func<Student, bool>> isTeenAgerExpr = s => s.Age > 12 && s.Age < 20;
//使用Compile方法编译Expression以将其作为委托调用
Func<Student, bool> isTeenAger = isTeenAgerExpr.Compile();
//Invoke
bool result = isTeenAger(new Student(){ StudentID = 1, StudentName = "Steve", Age = 20});

LINQ 表达式树

顾名思义,表达式树不过是按树状数据结构排列的表达式。表达式树中的每个节点都是一个表达式。例如,表达式树可用于表示数学公式x < y,其中x,< y 将表示为表达式,并排列在树状结构中。

表达式树是lambda表达式的内存表示形式。它保存查询的实际元素,而不是查询的结果。

表达式树使lambda表达式的结构透明和显式。您可以与表达式树中的数据进行交互,就像与其他任何数据结构一样。

例如,看以下isTeenAgerExpr表达式:

示例:用C#表达式

Expression<Func<Student, bool>> isTeenAgerExpr = s => s.age > 12 && s.age < 20;

编译器会将上面的表达式转换为以下表达式树:

示例:C#中的表达式树

Expression.Lambda<Func<Student, bool>>(
Expression.AndAlso(
Expression.GreaterThan(Expression.Property(pe, "Age"), Expression.Constant(12, typeof(int))),
Expression.LessThan(Expression.Property(pe, "Age"), Expression.Constant(20, typeof(int)))),
new[] { pe });

您也可以手动构建表达式树。让我们看看如何为以下简单的lambda表达式构建表达式树:

示例:C#中的Func委托:

Func<Student, bool> isAdult = s => s.age >= 18;

此Func类型委托将被视为以下方法:

C#:

public bool function(Student s)
{
return s.Age > 18;
}

要创建表达式树,首先,创建一个参数表达式,其中Student是参数的类型,'s'是参数的名称,如下所示:

步骤1:在C#中创建参数表达式

ParameterExpression pe = Expression.Parameter(typeof(Student), "s");

现在,使用Expression.Property()创建s.Age表达式,其中s是参数,Age是Student的属性名称。(Expression是一个抽象类,其中包含用于手动创建表达式树的静态帮助器方法。)

步骤2:在C#中创建属性表达式

MemberExpression me = Expression.Property(pe, "Age");

现在,为18创建一个常量表达式:

步骤3:在C#中创建常量表达式

ConstantExpression constant = Expression.Constant(18, typeof(int));

到目前为止,我们已经为s.Age(成员表达式)和18(常量表达式)构建了表达式树。现在,我们需要检查成员表达式是否大于常量表达式。为此,请使用Expression.GreaterThanOrEqual() 方法,并将成员表达式和常量表达式作为参数传递::

步骤4:在C#中创建二进制表达式

BinaryExpression body = Expression.GreaterThanOrEqual(me, constant);

因此,我们为lambda表达式主体 s.Age> = 18 构建了一个表达式树。我们现在需要将参数表达式和主体表达式连接起来。使用Expression.Lambda(body,parameters array)连接lambda表达式s => s.age> = 18的body(主体)和parameter(参数)部分:

步骤5:在C#中创建Lambda表达式

var isAdultExprTree = Expression.Lambda<Func<Student, bool>>(body, new[] { pe });

这样,您可以为带有lambda表达式的简单Func委托构建表达式树。

示例:C#中的表达式树

ParameterExpression pe = Expression.Parameter(typeof(Student), "s");
MemberExpression me = Expression.Property(pe, "Age");
ConstantExpression ce = Expression.Constant(18, typeof(int));
BinaryExpression body = Expression.GreaterThanOrEqual(me, ce);
var ExpressionTree = Expression.Lambda<Func<Student, bool>>(body, new[] { pe });
// Func<Student, bool> isAdult = s => s.Age >= 18;
// s: ParameterExpression
// s.Age: MemberExpression
// 18: ConstantExpression
// s.Age >= 18: Body as Binary Expression
// Func<Student, bool>: Expression Type
System.Console.WriteLine($"Expression Tree: {ExpressionTree}");
System.Console.WriteLine($"Expression Tree Body: {ExpressionTree.Body}");
System.Console.WriteLine($"Expression Tree's parameters count: {ExpressionTree.Parameters.Count}");
System.Console.WriteLine($"Expression Tree's first parameter: {ExpressionTree.Parameters[0]}");

输出:

Expression Tree: s => (s.Age >= 18)
Expression Tree Body: (s.Age >= 18)
Expression Tree's parameters count: 1
Expression Tree's first parameter: s

为什么选择表达树?

在Expression的讲述中,我们已经看到分配给lambda表达式Func编译为可执行代码,分配给lambda表达式Expression类型编译为Expression树。

可执行代码在同一个应用程序域中执行,以处理内存中的集合。可枚举的静态类包含用于实现IEnumerable 接口的内存中集合的扩展方法,例如List ,Dictionary 等。Enumerable类中的扩展方法接受Func类型委托的谓词参数。例如,Where扩展方法接受Func <TSource,bool>谓词。然后,将其编译为IL(中间语言)以处理同一AppDomain中的内存中集合。

Func委托是原始的可执行代码,因此,如果调试代码,则会发现Func委托将表示为不透明代码。您无法看到其参数,返回类型和主体。

Func委托用于内存中的集合,因为它将在同一个AppDomain中进行处理,但是诸如LINQ-to-SQL,EntityFramework或其他提供LINQ功能的第三方产品的远程LINQ查询提供者呢?他们将如何解析已编译为原始可执行代码的lambda表达式,以了解参数,lambda表达式的返回类型以及构建运行时查询以进一步处理?答案是表达树

Expression 被编译成称为表达式树的数据结构。

表达式树是透明的。您可以从表达式中检索参数,返回类型和主体表达式信息,如下所示:

示例:C#中的表达式树

Expression<Func<Student, bool>> isTeenAgerExpr = s => s.Age > 12 && s.Age < 20;
Console.WriteLine("Expression: {0}", isTeenAgerExpr );
Console.WriteLine("表达式类型: {0}", isTeenAgerExpr.NodeType);
var parameters = isTeenAgerExpr.Parameters;
foreach (var param in parameters)
{
Console.WriteLine("参数名称: {0}", param.Name);
Console.WriteLine("参数类型: {0}", param.Type.Name );
}
var bodyExpr = isTeenAgerExpr.Body as BinaryExpression;
Console.WriteLine("表达式主体左侧: {0}", bodyExpr.Left);
Console.WriteLine("二进制表达式类型: {0}", bodyExpr.NodeType);
Console.WriteLine("表达式主体右侧: {0}", bodyExpr.Right);
Console.WriteLine("返回类型: {0}", isTeenAgerExpr.ReturnType);

输出:

Expression: s => ((s.Age > 12) AndAlso (s.Age < 20))
表达式类型: Lambda
参数名称: s
参数类型: Student
表达式主体左侧: (s.Age > 12)
二进制表达式类型: AndAlso
表达式主体右侧: (s.Age < 20)
返回类型: System.Boolean

不在同一应用程序域中执行针对LINQ-to-SQL或Entity Framework的LINQ查询。例如,对于Entity Framework的以下LINQ查询永远不会在程序内部实际执行:

示例:C#中的LINQ查询

var query = from s in dbContext.Students
where s.Age >= 18
select s;

首先将其转换为SQL语句,然后在数据库服务器上执行。

在查询表达式中找到的代码必须转换为SQL查询,该查询可以作为字符串发送到另一个进程。对于LINQ-to-SQL或Entity Framework,该过程恰好是SQL Server数据库。将数据结构(如表达式树)转换为SQL显然比将原始IL或可执行代码转换为SQL容易得多,因为正如您看到的,从表达式中检索信息很容易。

创建表达式树的目的是将诸如查询表达式之类的代码转换为可以传递给其他进程并在此处执行的字符串。

可查询的静态类包括接受Expression类型的谓词参数的扩展方法。将该谓词表达式转换为表达式树,然后将其作为数据结构传递到远程LINQ提供程序,以便提供程序可以从表达式树构建适当的查询并执行查询。

posted @   非法关键字  阅读(133)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示