65.9K
CodeProject 正在变化。 阅读更多。
Home

LINQ 第 3 部分:IQueryable 简介

starIconstarIconstarIconstarIconstarIcon

5.00/5 (11投票s)

2018年4月20日

CPOL

9分钟阅读

viewsIcon

58402

downloadIcon

1157

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 表达式树

System.Linq.Enumerable 中的大多数方法只是创建 IEnumerable 的新实例,该实例包装了它所操作的实例。由于底层序列通常存在于内存中,或者很容易获取,因此无需真正担心获取它的机制。

System.Linq.Queryable 中的方法以不同的方式操作。每个方法都消耗一个 IQueryable,它只是将表达式树 (Expression) 与查询提供程序 (IQueryProvider) 配对。每个方法都会生成一个新的 IQueryable,其中表达式树已被修改以包含查询的其他元素。

让我们考虑一个简单的查询

Source.Students.Where(student => student.LastName == "Marx")

我们需要考虑的第一个 IQueryableSource.Students。在这种情况下,表达式树 (IQueryable.Expression) 完全由一个节点 (ConstantExpression) 组成。此节点的值 (ConstantExpression.Value) 就是可查询对象本身。

我们需要考虑的下一个 IQueryableWhere 方法返回的那个。这里事情变得复杂得多。在这种情况下,表达式树 (IQueryable.Expression) 由许多节点组成。从视觉上看,它显示如下

树中的节点如下所示

  1. Where 方法的调用,它接受两个参数,每个参数都出现在树中。
  2. Where 方法操作的原始 IQueryable
  3. Where 方法提供谓词的 Lambda 表达式的容器。注意:在我看来,这个节点是必需的,这似乎很奇怪。
  4. Where 方法提供谓词的 Lambda 表达式 (student => student.LastName == "Marx")。
  5. Lambda 表达式的参数 (student)。
  6. 在 Lambda 表达式中评估的“等于”比较 (student.LastName == "Marx")。
  7. “等于”比较的左操作数,它访问属性/成员 (LastName)。
  8. “等于”比较的右操作数,常量 "Marx"
  9. 属性/成员访问的参数 (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 窗体 TreeViewExpressionVisitor
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日 - 添加了本系列第四篇文章的链接
© . All rights reserved.