LINQ 第 3 部分:IQueryable 简介





5.00/5 (11投票s)
LINQ 系列的第三部分,本文介绍了 IQueryable、IQueryProvider 和 LINQ 表达式树。
引言
在本系列中,我们探讨了 IEnumerable
并研究了扩展此接口的标准方法。这些方法共同构成了 LINQ 的一小部分但至关重要的部分,称为 LINQ to Objects。
LINQ 的另一个重要方面是它能够查询其他数据源(例如数据库),其中信息不驻留在本地内存中。事实上,它甚至可能存在于完全不同的机器上。
为了支持这种非常不同的需求,LINQ 引入了查询提供程序 (System.Linq.IQueryProvider
)、表达式树 (System.Linq.Expressions.Expression
) 和可查询序列 (System.Linq.IQueryable
) 的概念。本文将探讨这些概念。
背景
对于 LINQ to Objects,LINQ 可以简单地提供直接在查询序列上操作的标准方法。对于其他数据源,这将是低效的。
例如,对于数据库,我们希望尽可能少地往返于服务器。此外,我们希望请求尽可能少的信息。最后,我们希望充分利用数据库产品的所有“智能”。
为方便起见,LINQ 引入了以下新概念
- 查询提供程序 (
IQueryProvider
) - 这是一种专门的软件,可以解释查询,从而高效地利用底层资源。 - 表达式树 (
Expression
) - 树由查询元素构成。该树稍后由查询提供程序分析。这对于编写过解析器或编译器的人来说应该很熟悉。 - 可查询序列 (
IQueryable
) - 这大致相当于 LINQ to Objects 中的IEnumerable
。它只是将查询提供程序与表达式树配对。
与 IEnumerable
一样,LINQ 提供了一组标准方法,定义在 System.Linq.Queryable
类中。这些方法都扩展了 IQueryable
。它们与 System.Linq.Enumerable
中的对应方法共享相同的名称和几乎相同的语法。
虽然这降低了开发人员的学习曲线,但它也具有欺骗性。虽然概念上相似,但这些方法以完全不同的方式实现其目标:它们构建表达式树。
这是 LINQ 系列文章的第三篇。本系列中其他文章的链接如下
- LINQ 第 1 部分:深入了解 IEnumerable
- LINQ 第 2 部分:标准方法 - 工具箱中的工具
- LINQ 第 3 部分:IQueryable 简介
- LINQ 第 4 部分:深入了解 Queryable 扩展方法
LINQ 表达式树
System.Linq.Enumerable
中的大多数方法只是创建 IEnumerable
的新实例,该实例包装了它所操作的实例。由于底层序列通常存在于内存中,或者很容易获取,因此无需真正担心获取它的机制。
System.Linq.Queryable
中的方法以不同的方式操作。每个方法都消耗一个 IQueryable
,它只是将表达式树 (Expression
) 与查询提供程序 (IQueryProvider
) 配对。每个方法都会生成一个新的 IQueryable
,其中表达式树已被修改以包含查询的其他元素。
让我们考虑一个简单的查询
Source.Students.Where(student => student.LastName == "Marx")
我们需要考虑的第一个 IQueryable
是 Source.Students
。在这种情况下,表达式树 (IQueryable.Expression
) 完全由一个节点 (ConstantExpression
) 组成。此节点的值 (ConstantExpression.Value
) 就是可查询对象本身。
我们需要考虑的下一个 IQueryable
是 Where
方法返回的那个。这里事情变得复杂得多。在这种情况下,表达式树 (IQueryable.Expression
) 由许多节点组成。从视觉上看,它显示如下
树中的节点如下所示
- 对
Where
方法的调用,它接受两个参数,每个参数都出现在树中。 Where
方法操作的原始IQueryable
。- 为
Where
方法提供谓词的 Lambda 表达式的容器。注意:在我看来,这个节点是必需的,这似乎很奇怪。 - 为
Where
方法提供谓词的 Lambda 表达式 (student => student.LastName == "Marx"
)。 - Lambda 表达式的参数 (
student
)。 - 在 Lambda 表达式中评估的“等于”比较 (
student.LastName == "Marx"
)。 - “等于”比较的左操作数,它访问属性/成员 (
LastName
)。 - “等于”比较的右操作数,常量
"Marx"
。 - 属性/成员访问的参数 (
student
)。这是找到属性值的特定实例。
表达式树有什么用?
现在,我们有了一个漂亮的表达式树。它有什么用?
这就是查询提供程序 (IQueryProvider
) 发挥作用的地方。它负责“执行”表达式树描述的查询。
例如,对于数据库提供程序,它可能会做很多事情。首先,它需要将表达式树转换为 SQL 查询。因此,在我们之前的示例中,这可能类似于
SELECT * FROM Students WHERE LastName = 'Marx'
然后,它可能需要在数据库服务器上执行查询,获取结果,为每一行实例化对象,并提供这些对象的枚举。
快速回顾
从本文中,我们学到了以下内容
System.Linq.Queryable
中的扩展方法通过构建表达式树 (Expression
) 对IQueryable
实例进行操作。IQueryable
只是将表达式树 (Expression
) 与查询提供程序 (IQueryProvider
) 配对。- 查询提供程序 (
IQueryProvider
) 负责解释表达式树、执行查询和获取结果。
这里的关键词是“解释”。由于查询提供程序(及其底层资源)可能比 C# 语言更受限制,因此查询中可以出现的内容也有限制。
通常,Lambda 表达式仅限于相当普通的运算:比较、字符串操作和投影(根据现有属性值创建新实例)。确切的限制取决于查询提供程序本身。
如果您只是使用 LINQ 可查询对象,那么到目前为止所学到的知识就是您需要理解的全部内容。
本文的其余部分将深入探讨。您可以根据自己的具体需求选择进一步学习或跳过。
探索更复杂的查询
到目前为止,我们只看了一个非常简单的查询。随着查询复杂度的增加,表达式树会变得相当大。为了方便探索更复杂的查询,本文附带了一个应用程序 (QueryableFun
)。
我建议您简单地玩一下这个应用程序。它允许用户运行一些预定义的查询。这些查询是从窗口右上角工具条中的“Queries”下拉列表中选择的。
要执行查询,只需单击“Run”按钮()。
该应用程序由两个面板组成:表达式树(左侧)和选项卡控件(右侧)。在选项卡控件中,您会找到四个选项卡。它们如下
- Expression - 在此处输入或查看 C# LINQ 表达式。
- Properties - 当您单击表达式树中的节点时,此选项卡将显示该节点的属性。
- Results - 一个包含查询结果的数据网格。
- Errors - 在评估表达式时发生的任何编译错误。
下面显示了应用程序的屏幕截图。请理解这只是一个学习应用程序。它不适用于任何生产用途。因此,它的质量低于我通常会生产的水平。
对于 LINQ 的认真探索,我强烈推荐 LINQPad。我与该公司没有任何关联。我只是在自学 LINQ 时非常依赖它。该应用程序提供免费版和付费版。
IQueryable 成员
虽然这是最重要的接口,但它也是最简单的接口。它的主要目的是将表达式树与查询提供程序配对。事实上,它非常简单,单个实现就可以满足大多数查询提供程序的需求。
它只有四个成员,如下所示
成员名称 | 描述 |
---|---|
表达式 | 这是此可查询对象的表达式树。 |
元素类型 | 构成序列的元素类型。 |
提供商 | 负责解释和执行查询的查询提供程序。 |
GetEnumerator<TElement> | 获取此可查询对象的强类型枚举器 (IEnumerator<TElement> )。这通常会调用查询提供程序来解释和执行查询。 |
GetEnumerator | 获取此可查询对象的弱类型枚举器 (IEnumerator )。通常,这只是调用 GetEnumerator<TElement>() 。 |
IQueryProvider 成员
虽然此接口的实现通常相当复杂,但接口本身却相当简单。它允许使用者创建可查询对象并执行表达式。它只有四个成员
成员名称 | 描述 |
---|---|
CreateQuery() | 为指定的表达式创建弱类型可查询对象 (IQueryable )。 |
CreateQuery<TElement>() | 为指定的表达式创建强类型可查询对象 (IQueryable<TElement> )。 |
Execute() | 执行指定的表达式并返回弱类型结果。 |
Execute<TResult>() | 执行指定的表达式并返回强类型结果。 |
QueryableFun 模块
QueryableFun
应用程序主要旨在提供表达式树的外观。虽然它不是一项非常认真的工作,但它确实展示了一些巧妙的技巧。
- Roslyn - 它提供了一个使用 Microsoft 的 Roslyn 技术编译和执行 C# 脚本的示例。
IQueryable
- 它提供了一个非常简单的IQueryable
实现示例。此实现没有针对外部资源评估和执行查询的复杂性,而是简单地包装了一个底层集合。IQueryProvider
- 它提供了一个非常简单的IQueryProvider
实现示例。此实现只是修改表达式树,将原始可查询对象替换为底层集合的可查询对象。然后,它巧妙地利用LambdaExpression.Compile()
来完成所有繁重的工作。- 玩具数据 - 它为 LINQ 查询提供了一些“玩具”数据,因此不需要数据库或更复杂的资源。
此应用程序中的模块如下
模块名称 | 描述 |
---|---|
EnumerableExtensions | IEnumerable 的扩展,可简化查找枚举的元素类型。 |
ExpressionTreeBuilder | 一个填充 Windows 窗体 TreeView 的 ExpressionVisitor 。 |
ExpressionTreeRemediator | 一个 ExpressionVisitor ,它将包含原始 IQueryable 的节点替换为底层集合的新 IQueryable 。 |
InterestingQueries | 一组预定义查询,演示了一些常见的用例。 |
MainAboutBox | 一个相当标准的“帮助 关于...”对话框。 |
MainForm | 应用程序的用户界面。 |
程序 | 应用程序的主要启动点。 |
QueryableEnumerable | IQueryable 的一个实现,它只是包装了一个底层集合。 |
QueryableEnumerableProvider | IQueryProvider 的一个实现,它使用 LambdaExpression.Compile() 来完成所有繁重的工作。 |
来源 | LINQ 查询的静态源,可以在用户提供的表达式中轻松引用 |
在 Transeric.Queryable
项目中,该项目也是此应用程序的一部分,以简单的 POCO 类和基于它们的集合的形式提供了“玩具”数据。
延伸阅读
如需进一步阅读,请参阅以下内容
IQueryable<T> 接口
https://docs.microsoft.com/zh-cn/dotnet/api/system.linq.iqueryable-1
IQueryProvider 接口
https://docs.microsoft.com/zh-cn/dotnet/api/system.linq.iqueryprovider
Expression 类
https://docs.microsoft.com/zh-cn/dotnet/api/system.linq.expressions.expression
Queryable 类
https://docs.microsoft.com/zh-cn/dotnet/api/system.linq.queryable
LINQ to Objects
https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/linq/linq-to-objects
LINQ to SQL
https://docs.microsoft.com/zh-cn/dotnet/framework/data/adonet/sql/linq/index
LINQPad
https://www.linqpad.net/
历史
- 2018年4月20日 - 上传原始版本
- 2018年4月21日 - 添加了本系列其他文章的链接
- 2018年4月23日 - 更新了 exe 和 src 以处理单例 LINQ 方法
- 2018年4月24日 - 进行了几次小的外观和语法更正
- 2018年4月25日 - 添加了本系列第四篇文章的链接