C# 中的 IEnumerable 与 IQueryable

2024 年 8 月 29 日 | 阅读 12 分钟

在 C# 中,IEnumerableIQueryable 都是在 LINQ (Language Integrated Query) 中用于处理数据集合的接口,但它们的功能和特性有所不同。

什么是 IEnumerable?

IEnumerable 是一个基本接口,它表示一个数据的前向光标。它用于查询和操作内存中的集合,例如数组、列表以及实现此接口的其他数据结构。

IEnumerableSystem.Collection 命名空间的一部分,用于表示一组可枚举(迭代)的对象集合。它是处理集合的基本接口,提供了一种标准的方法来迭代集合中的元素,而无需暴露底层数据结构。

立即执行:IEnumerable 序列执行的操作会立即生效。当你在 IEnumerable 上执行 LINQ 操作时,数据会在内存中被查询和处理,结果会作为新的序列或集合返回。

无延迟执行: 操作不是延迟的;它们在你调用 LINQ 方法(例如 Where、Select、ToList、ToArray 等)时立即执行。

适用于内存中集合: 它非常适合处理内存中的数据集合,因为整个数据集都可以装入内存。

LINQ to Objects: 它主要用于 LINQ to Objects,这意味着它处理的是已经加载到内存中的集合。

一定程度的强类型

  • IEnumerable 不如 IQueryable 强类型。它以通用的方式处理对象集合。
  • 类型安全是存在的,但在处理对象时可能需要显式转换。

示例

特性

它具有 C# 中 IEnumerable 的几个特性。 IEnumerable 的一些主要特性如下:

集合无关性

IEnumerable 是一种以通用、集合无关的方式与对象集合交互的方式。无论你处理的是数组、列表还是自定义集合,只要它实现了 IEnumerable,你都可以使用一组一致的方法来处理它。

迭代抽象

IEnumerable 提供了一种迭代抽象,而不是直接操作集合的底层数据结构。它允许你向集合请求一个枚举器 (IEnumerator),该枚举器知道如何遍历元素。

foreach 循环兼容性

IEnumerable 的一个关键优势在于它能够使用 foreach 循环轻松遍历集合中的元素。此循环依赖 GetEnumerator 方法获取枚举器,然后使用该枚举器遍历元素,而无需了解特定的集合类型。

LINQ 集成

IEnumerable 与 LINQ 紧密集成,LINQ 是 C# 中用于处理集合的强大查询语言。实现 IEnumerable 的集合可以利用 LINQ 丰富的扩展方法集来执行过滤、排序和投影等操作。

自定义迭代

你可以通过实现 IEnumerable 来使自己的类可迭代。当你有一个自定义集合并希望启用标准的迭代模式时,这很有帮助。实现 IEnumerable 意味着你需要提供一个枚举器,该枚举器定义了如何遍历集合中的元素。

基本 LINQ 支持

  • IEnumerable 提供了一组基本的 LINQ 运算符,这些运算符对于处理内存中的集合很有用。
  • 支持常见的 LINQ 操作,如 Where、Select、OrderBy 和 GroupBy。

有限的查询优化

  • 由于操作是立即在客户端执行的,因此查询优化有限。
  • 操作不会被转换为更高效的查询语言(如 SQL)来处理外部数据源。
  • 总而言之,IEnumerable 提供了一种统一的方式来处理集合,无论它们是 .NET Framework 标准库的一部分,还是你自己定义的自定义数据结构。它抽象了遍历元素的过程,使你的代码更具通用性和适应性,能够处理不同类型的集合。这种灵活性是 C# 处理数据方式的基础。

程序

让我们用一个例子来说明 C# 中的 iEnumerable

输出

Name: Alice, Age: 30
Name: Bob, Age: 25
Name: Charlie, Age: 35

说明

  • 在此示例中,Person 类表示具有 Name 和 Age 两个属性的个体。它是一个简单的结构,用于存储关于人的信息。
  • PeopleCollection 是一个自定义集合类,它存储 Person 对象的列表。
  • 它实现了 IEnumerable 接口,表明它包含一个可枚举的 Person 对象序列。
  • AddPerson 方法允许你添加新的 Person 对象到集合中。
  • GetEnumerator 方法对于使集合可迭代至关重要。它为内部的 people 列表返回一个枚举器。此枚举器用于遍历集合。
  • 我们创建了一个名为 peopleCollection 的 PeopleCollection 实例。
  • 我们使用 AddPerson 方法向集合中添加三个人:"Alice""Bob""Charlie",并附带他们各自的年龄。
  • 之后,我们使用 foreach 循环遍历 peopleCollection 对象。这是可能的,因为 PeopleCollection 实现了 IEnumerable。

复杂度分析

时间复杂度

将 Person 添加到 PeopleCollection:O(1)

将 Person 添加到 PeopleCollection 时,它会直接将其追加到内部列表中。这是一个 O(1) 操作,因为它不依赖于集合的大小。

使用 foreach 遍历 PeopleCollection:O(n)

当你使用 foreach 循环遍历 PeopleCollection 时,它本质上涉及遍历集合中的每个元素。时间复杂度为 O(n),其中 'n' 是集合中 Person 对象的数量。

空间复杂度

PeopleCollection 的空间复杂度:O(n)

PeopleCollection 类内部使用 List 来存储 Person 对象。空间复杂度为 O(n),其中 'n' 是集合中存储的 Person 对象的数量。

Person 对象空间复杂度:每个 Person 对象 O(1)

每个 Person 对象都有为 Name 和 Age 属性分配的固定内存量。因此,每个 Person 对象的空间复杂度为 O(1)

总而言之,将 Person 添加到集合的时间复杂度为 O(1),遍历集合的时间复杂度为 O(n)。PeopleCollection 类的空间复杂度为 O(n),每个 Person 对象的空间复杂度为 O(1)。

什么是 IQueryable?

IQueryable 是一个扩展了 IEnumerable 的接口,专为从支持查询的数据源(如数据库)查询数据而设计。它是 LINQ to SQL、LINQ to Entities 以及其他 LINQ 提供程序的一部分。

延迟执行

IQueryable 允许你创建不会在构造时立即执行的查询。相反,只有当你显式请求结果时,它们才会被执行。

这种延迟执行是一项基本功能,因为它允许查询优化并最大限度地减少从数据源传输的数据量。

查询优化: 它允许进行查询优化,这意味着查询提供程序在处理数据库时可以将 LINQ 查询转换为高效的 SQL(或等效)查询。

适用于远程数据源: 它非常适合处理可以处理查询操作的数据源,如数据库。查询在远程数据源上执行,从而减少传输的数据量。

LINQ to SQL、LINQ to Entities: 它主要用于 LINQ to SQL、LINQ to Entities 或其他 LINQ 提供程序,你可以在其中处理存储在数据库或远程服务中的数据。

示例

特性

它具有 C# 中 IQueryable 的几个特性。IQueryable 的一些主要特性如下:

与数据源集成

  • IQueryable 通常与数据源一起使用,例如数据库(例如 SQL Server)和对象关系映射 (ORM) 框架(例如 Entity Framework)。
  • 它允许你用 C# 表达查询,这些查询可以被翻译成 SQL 或其他查询语言,以便在数据存储上高效执行。

类型安全

  • 使用 IQueryable 创建的查询是强类型的。这意味着你获得编译时类型检查,从而减少了运行时错误的发生。
  • 你可以使用强类型实体和属性,提供 IntelliSense 支持并提高代码可读性。

查询组合

  • IQueryable 允许你通过以可读且模块化的方式链接多个查询运算符(例如 Where、OrderBy、Select)来构建复杂的查询。
  • 它促进了查询代码的可维护性和表达力。

表达式树

  • IQueryable 将查询表示为表达式树,表达式树是表示代码逻辑的抽象语法树。
  • 这些表达式树可以被分析和修改,从而实现高级查询优化和转换。

自定义查询提供程序

  • 你可以创建自定义查询提供程序来扩展 IQueryable,使其能够处理数据库之外的各种数据源,例如内存中的集合、Web 服务或其他数据存储。
  • 这使得 IQueryable 能够高度适应不同的场景。

外部数据源集成

  • IQueryable 专为查询外部数据源而设计,例如数据库、Web 服务和其他数据存储。
  • 它可以将 LINQ 查询转换为本机查询语言(例如 SQL),以便从这些源高效检索数据。

异步支持

  • 支持查询的异步执行,从而在处理外部数据源时实现并行处理和提高性能。
  • 提供了 ToListAsyncAsync 等异步方法来支持异步操作。
  • 它涉及查询优化,将 LINQ 查询转换为高效检索外部数据源数据的本机查询语言。
  • 查询优化可以显著提高处理大型数据集和数据库时的性能。

程序

让我们用一个例子来说明 C# 中的 IQueryable

输出

People over 30:
Charlie, 35 years old

Names of all people:
Alice
Bob
Charlie
David
Eve

Sum of ages: 140 years

说明

Person 类

我们定义了一个简单的 Person 类,包含 Name 和 Age 两个属性,用于表示个体。这个类将用于创建保存与人员相关数据的对象。

People 列表

我们创建了一个名为 people 的列表,并用 Person 类的实例填充它。这个列表代表了一个人集合,每个人都有一个名字和一个年龄。

IQueryable 创建

我们通过使用 AsQueryable 方法将 people 列表转换为 IQueryable。此转换允许我们对列表使用 LINQ 运算符,将其视为可查询的数据源。

查询操作

我们对 IQueryable 对象执行各种查询操作:

过滤 (Where): 我们创建了一个名为 over 30IQueryable,它代表了一个查找年龄大于 30 的人的查询。

投影 (Select): 我们创建了一个名为 names 的 IQueryable,它代表了一个提取所有人名字的查询。

聚合 (Sum): 我们使用 Sum 运算符计算年龄总和,结果是一个名为 ageSum 的 int。

结果执行

我们通过迭代结果来显式执行查询。这时查询会被执行,数据被检索。

  • 我们将年龄大于 30 的人的姓名和年龄打印到控制台。
  • 我们将所有人的姓名打印到控制台。
  • 我们将年龄总和打印到控制台。

复杂度分析

时间复杂度

创建和填充 people 列表

时间复杂度:O(n)

用 n 个项填充列表需要线性时间,因为每个人是逐个添加到列表中的。

从 people 创建 IQueryable

时间复杂度:O(1)

使用 AsQueryable 将 people 列表转换为 IQueryable 是一个常数时间操作,不依赖于列表的大小。

查询操作 (Where, Select, Sum)

时间复杂度:O(n)

使用 LINQ 执行查询操作,例如 Where、Select 和 Sum,通常需要对整个数据源进行一次遍历。这些操作的时间复杂度与数据源的大小成线性关系。

遍历结果

时间复杂度:O(n)

遍历查询操作的结果也需要线性时间,因为它需要访问结果集中的每个项。

空间复杂度

people 列表:O(n)

people 列表的空间复杂度与存储在列表中的人数成线性关系。每个 Person 对象占用固定的内存量。

IQueryable 和查询运算符:O(1)

创建 IQueryable 和定义查询运算符不会显著增加内存使用量。使用的空间是常数,不依赖于数据源的大小。

查询结果 (over30, names, ageSum):O(m)

查询结果(over30、names 和 ageSum)的空间复杂度取决于结果集的大小。如果结果集包含 m 项,则空间复杂度为 O(m)

空间复杂度取决于结果集的大小和原始 people 列表,列表的空间复杂度为 O(n),查询结果的空间复杂度为 O(m),其中 "n" 是列表中的人数,"m" 是每个查询中的结果数。

IEnumerable 和 IQueryable 的主要区别

IEnumerableIQueryable 之间存在几个区别。IEnumerable 和 IQueryable 之间的一些主要区别如下:

目的

IEnumerable

  • 它主要用于内存中的集合,例如数组、列表和其他集合。
  • IEnumerable 适用于处理已在内存中的数据,可用于集合的一般迭代和操作。

IQueryable

  • 它专为从各种数据源(如数据库、Web 服务或其他外部数据存储)查询数据而设计。
  • IQueryable 用于构建和执行针对外部数据源的复杂查询,并支持延迟执行。

立即执行与延迟执行

IEnumerable

  • IEnumerable 集合的操作在调用方法时立即执行。没有查询优化或延迟执行。
  • 它适用于内存中的集合,因为所有数据都已准备就绪。

IQueryable

  • IQueryable 支持延迟执行。查询操作直到显式请求结果时才执行。
  • 这种延迟执行支持查询优化,并减少了从外部数据源传输的数据量。

数据源

IEnumerable

  • 它通常用于内存中的集合,如数组、列表和字典。
  • 它不适合查询外部数据源(如数据库)。

IQueryable

  • 它专为查询外部数据源而设计,包括数据库、Web 服务和自定义数据存储。
  • 它支持将 LINQ 查询转换为本机查询语言(例如 SQL)以进行高效数据检索。

强类型

IEnumerable

  • 它以通用的方式处理对象集合。
  • 它类型安全性较低,因为它操作对象并可能需要转换。

IQueryable

  • 它提供强类型,并以强类型的方式与实体和属性一起使用。
  • 类型安全使其更容易在编译时捕获错误。

优化

IEnumerable

  • 由于操作是立即执行的,因此不涉及查询优化。
  • 在筛选或投影之前,可能需要将所有数据加载到内存中。

IQueryable

  • 它涉及查询优化和延迟执行。查询提供程序会优化并将其转换为数据源最有效的形式。
  • 它最大限度地减少了数据传输,并提高了查询效率,尤其是在查询数据库时。

与 LINQ 集成

IEnumerable

  • 它提供了一组基本的 LINQ 运算符,用于处理内存中的集合。
  • LINQ 操作在客户端执行。

IQueryable

  • 它提供了一组扩展的 LINQ 运算符,用于处理复杂查询。
  • 使用 IQueryable 的 LINQ 查询被转换为本机查询语言(例如 SQL),并在数据源端执行。

自定义数据源

IEnumerable

  • 它通常用于内存中的集合,因此不太适合自定义数据源。
  • 查询非集合数据源需要自定义逻辑。

IQueryable

  • 可以通过创建自定义查询提供程序来扩展它,使其能够处理自定义数据源。
  • 它允许查询比集合和数据库更广泛的数据源。

结论

总而言之,IEnumerable 最适合内存中的集合和操作的即时执行,而 IQueryable 则专为具有延迟执行和查询优化的外部数据源的查询而设计。选择哪一个取决于你的数据性质和应用程序的具体需求。