龙空技术网

c# 10 教程:8 Linq查询

启辰8 63

前言:

现时兄弟们对“c语言函数void fun寻找元音字母”大体比较关心,咱们都需要了解一些“c语言函数void fun寻找元音字母”的相关内容。那么小编在网摘上网罗了一些关于“c语言函数void fun寻找元音字母””的相关知识,希望我们能喜欢,小伙伴们快快来学习一下吧!

LINQ 或语言集成查询是一组语言和运行时功能,用于编写针对本地对象集合和远程数据源的结构化类型安全查询。

LINQ 使您能够查询实现 IEnumerable<T> 的任何集合,无论是数组、列表还是 XML 文档对象模型 (DOM),以及远程数据源(如 SQL Server 数据库中的表)。LINQ 提供了编译时类型检查和动态查询组合的优点。

本章介绍 LINQ 体系结构和编写查询的基础知识。所有核心类型都在 System.Linq 和 System.Linq.Expressions 命名空间中定义。

注意

本章和以下两章中的示例预加载到名为 LINQPad 的交互式查询工具中。您可以从 下载 LINQPad。

开始

LINQ 中数据的基本单位是和。序列是实现 IEnumerable<T> 的任何对象,元素是序列中的每个项目。在下面的示例中,names 是一个序列,“Tom”、“Dick”和“Harry”是:

string[] names = { "Tom", "Dick", "Harry" };

我们称之为本地序列,因为它表示内存中对象的集合。

是一种转换序列的方法。典型的查询运算符接受序列并发出转换后的。在 System.Linq 的 Enumerable 类中,大约有 40 个查询运算符 — 全部作为静态扩展方法实现。这些运算符称为。

注意

对本地序列进行操作的查询称为本地查询或 查询。

LINQ 还支持可从远程数据源(如 SQL Server 数据库)动态馈送的序列。这些序列还实现了 IQueryable<T> 接口,并通过 Queryable 类中的一组匹配的标准查询运算符提供支持。我们将在中进一步讨论这个问题。

查询是一个表达式,在枚举时,它使用查询运算符转换序列。最简单的查询包括一个输入序列和一个运算符。例如,我们可以在简单数组上应用 Where 运算符来提取长度至少为四个字符的字符串,如下所示:

string[] names = { "Tom", "Dick", "Harry" };IEnumerable<string> filteredNames = System.Linq.Enumerable.Where                                    (names, n => n.Length >= 4);foreach (string n in filteredNames)  Console.WriteLine (n);DickHarry

因为标准查询运算符是作为扩展方法实现的,所以我们可以直接在名称上调用 Where ,就好像它是一个实例方法一样:

IEnumerable<string> filteredNames = names.Where (n => n.Length >= 4);

为此,必须导入 System.Linq 命名空间。下面是一个完整的示例:

using System;using System.Collections.Generic;using System.Linq;string[] names = { "Tom", "Dick", "Harry" };IEnumerable<string> filteredNames = names.Where (n => n.Length >= 4);foreach (string name in filteredNames) Console.WriteLine (name);DickHarry
注意

我们可以通过隐式键入filteredNames来进一步缩短代码:

var filteredNames = names.Where (n => n.Length >= 4);

但是,这可能会妨碍 IDE 之外的可读性,因为 IDE 没有工具提示可以提供帮助。出于这个原因,我们在本章中使用隐式类型比在您自己的项目中使用更少。

大多数查询运算符接受 lambda 表达式作为参数。lambda 表达式有助于指导和调整查询。在我们的示例中,lambda 表达式如下所示:

n => n.Length >= 4

输入参数对应于输入元素。在这种情况下,输入参数 n 表示数组中的每个名称,并且类型为 字符串 。Where 运算符要求 lambda 表达式返回一个布尔值,如果为 true,则表示该元素应包含在输出序列中。这是它的签名:

public static IEnumerable<TSource> Where<TSource>  (this IEnumerable<TSource> source, Func<TSource,bool> predicate)

以下查询提取包含字母“a”的所有名称:

IEnumerable<string> filteredNames = names.Where (n => n.Contains ("a"));foreach (string name in filteredNames)  Console.WriteLine (name);             // Harry

到目前为止,我们已经使用扩展方法和 lambda 表达式构建了查询。正如您稍后将看到的,此策略是高度可组合的,因为它允许查询运算符的链接。在本书中,我们将其称为。1C# 还提供了另一种用于编写查询的语法,称为语法。下面是我们前面作为查询表达式编写的查询:

IEnumerable<string> filteredNames = from n in names                                    where n.Contains ("a")                                    select n;

流畅的语法和查询语法是互补的。在接下来的两节中,我们将更详细地探讨每一节。

流畅的语法

流畅的语法是最灵活和最基本的。在本节中,我们将介绍如何链接查询运算符以形成更复杂的查询,并说明为什么扩展方法对此过程很重要。我们还描述了如何为查询运算符制定 lambda 表达式,并介绍了几个新的查询运算符。

链接查询运算符

在上一节中,我们展示了两个简单的查询,每个查询包含一个查询运算符。若要生成更复杂的查询,请将其他查询运算符追加到表达式中,从而创建链。为了说明这一点,以下查询提取包含字母“a”的所有字符串,按长度对它们进行排序,然后将结果转换为大写:

using System;using System.Collections.Generic;using System.Linq;string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };IEnumerable<string> query = names  .Where   (n => n.Contains ("a"))  .OrderBy (n => n.Length)  .Select  (n => n.ToUpper());foreach (string name in query) Console.WriteLine (name);JAYMARYHARRY
注意

在我们的示例中,变量 n 私有作用域为每个 lambda 表达式。我们可以重用标识符 n,原因与我们可以在以下方法中重用标识符 c 的原因相同:

void Test(){  foreach (char c in "string1") Console.Write (c);  foreach (char c in "string2") Console.Write (c);  foreach (char c in "string3") Console.Write (c);}

其中、OrderBy 和 Select 是标准查询运算符,它们解析为 Enumerable 类中的扩展方法(如果导入 System.Linq 命名空间)。

我们已经引入了 Where 运算符,它发出输入序列的过滤版本。OrderBy 运算符发出其输入序列的排序版本;Select 方法发出一个序列,其中每个输入元素都使用给定的 lambda 表达式进行转换或(在本例中为 n.ToUpper() )。数据通过运算符链从左到右流动,因此首先对数据进行筛选,然后进行排序,然后进行投影。

注意

查询运算符从不更改输入序列;相反,它返回一个新序列。这与启发 LINQ 的范例一致。

以下是每种扩展方法的签名(OrderBy 签名略有简化):

public static IEnumerable<TSource> Where<TSource>  (this IEnumerable<TSource> source, Func<TSource,bool> predicate)public static IEnumerable<TSource> OrderBy<TSource,TKey>  (this IEnumerable<TSource> source, Func<TSource,TKey> keySelector)public static IEnumerable<TResult> Select<TSource,TResult>  (this IEnumerable<TSource> source, Func<TSource,TResult> selector)

当查询运算符链接(如本例所示)时,一个运算符的输出序列是下一个运算符的输入序列。完整的查询类似于传送带生产线,如图 所示。

链接查询运算符

我们可以构造相同的查询,如下所示:

// You must import the System.Linq namespace for this to compile:IEnumerable<string> filtered   = names   .Where   (n => n.Contains ("a"));IEnumerable<string> sorted     = filtered.OrderBy (n => n.Length);IEnumerable<string> finalQuery = sorted  .Select  (n => n.ToUpper());

finalQuery 在组合上与我们之前构造的查询相同。此外,每个中间步骤还包括一个我们可以执行的有效查询:

foreach (string name in filtered)  Console.Write (name + "|");        // Harry|Mary|Jay|Console.WriteLine();foreach (string name in sorted)  Console.Write (name + "|");        // Jay|Mary|Harry|Console.WriteLine();foreach (string name in finalQuery)  Console.Write (name + "|");        // JAY|MARY|HARRY|
为什么扩展方法很重要

可以使用传统的静态方法语法来调用查询运算符,而不是使用扩展方法语法:

IEnumerable<string> filtered = Enumerable.Where (names,                                                 n => n.Contains ("a"));IEnumerable<string> sorted = Enumerable.OrderBy (filtered, n => n.Length);IEnumerable<string> finalQuery = Enumerable.Select (sorted,                                                    n => n.ToUpper());

事实上,这就是编译器转换扩展方法调用的方式。但是,如果要像我们之前所做的那样在单个语句中编写查询,则避免使用扩展方法是有代价的。让我们重新审视单语句查询 — 首先是扩展方法语法:

IEnumerable<string> query = names.Where   (n => n.Contains ("a"))                                 .OrderBy (n => n.Length)                                 .Select  (n => n.ToUpper());

其自然的线性形状反映了从左到右的数据流,并且还保留了 lambda 表达式与其查询运算符(表示法)一起。如果没有扩展方法,查询将失去:

IEnumerable<string> query =  Enumerable.Select (    Enumerable.OrderBy (      Enumerable.Where (        names, n => n.Contains ("a")      ), n => n.Length    ), n => n.ToUpper()  );
编写 Lambda 表达式

在前面的示例中,我们将以下 lambda 表达式提供给 Where :

n => n.Contains ("a")      // Input type = string, return type = bool.
注意

接受值并返回布尔值的 lambda 表达式称为。

lambda 表达式的用途取决于特定的查询运算符。使用 Where 运算符,它指示元素是否应包含在输出序列中。对于 OrderBy 运算符,lambda 表达式将输入序列中的每个元素映射到其排序键。使用 Select 运算符,lambda 表达式确定输入序列中的每个元素在馈送到输出序列之前如何转换。

注意

查询运算符中的 lambda 表达式始终适用于输入序列中的单个元素,而不是整个序列。

查询运算符根据需要计算 lambda 表达式,通常对输入序列中的每个元素计算一次。Lambda 表达式允许您将自己的逻辑馈送到查询运算符中。这使得查询运算符具有多功能性,并且在引擎盖下也很简单。下面是 Enumerable.Where 的完整实现,除了异常处理:

public static IEnumerable<TSource> Where<TSource>  (this IEnumerable<TSource> source, Func<TSource,bool> predicate){  foreach (TSource element in source)    if (predicate (element))      yield return element;}
Lambda 表达式和 Func 签名

标准查询运算符使用泛型 Func 委托。Func 是 System 命名空间中的一系列通用泛型委托,定义意图如下:

Func 中的类型参数的显示顺序与它们在 lambda 表达式中的顺序相同。

因此,Func<Sucherce,bool> 匹配 TSource=>bool lambda 表达式:一个接受 TSource 参数并返回布尔值的表达式。

类似地,Func<TSource,TResult> 匹配 TSource=>TResult lambda 。

Func 委托列在中。

Lambda 表达式和元素类型

标准查询运算符使用以下类型参数名称:

通用类型字母

意义

Ts'ource

输入序列的元素类型

特雷苏尔特

输出序列的元素类型(如果与输出不同)

特雷

用于排序、分组或联接的的元素类型

由输入序列决定。TResult 和 TKey 通常是。

例如,考虑 Select 查询运算符的签名:

public static IEnumerable<TResult> Select<TSource,TResult>  (this IEnumerable<TSource> source, Func<TSource,TResult> selector)

Func<TSource,TResult> 匹配 TSource=>TResult lambda 表达式:将元素映射到的表达式。S 和 TResult 可以是不同的类型,因此 lambda 表达式可以更改每个元素的类型。此外,lambda 表达式。以下查询使用 Select 将字符串类型元素转换为整数类型元素:

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };IEnumerable<int> query = names.Select (n => n.Length);foreach (int length in query)  Console.Write (length + "|");    // 3|4|5|4|3|

编译器 lambda 表达式的返回值推断 TResult 的类型。在这种情况下,n.Length 返回一个 int 值,因此 TResult 被推断为 int 。

Where 查询运算符更简单,不需要对输出进行类型推断,因为输入和输出元素的类型相同。这是有道理的,因为运算符只是过滤元素;它不会它们:

public static IEnumerable<TSource> Where<TSource>  (this IEnumerable<TSource> source, Func<TSource,bool> predicate)

最后,考虑 OrderBy 运算符的签名:

// Slightly simplified:public static IEnumerable<TSource> OrderBy<TSource,TKey>  (this IEnumerable<TSource> source, Func<TSource,TKey> keySelector)

Func<The,TKey>将输入元素映射到。TKey 是从您的 lambda 表达式推断出来的,并且独立于输入和输出元素类型。例如,我们可以选择按长度(int键)或字母顺序(字符串键)对名称列表进行排序:

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };IEnumerable<string> sortedByLength, sortedAlphabetically;sortedByLength       = names.OrderBy (n => n.Length);   // int keysortedAlphabetically = names.OrderBy (n => n);          // string key
注意

您可以使用引用方法而不是 lambda 表达式的传统委托调用 Enumerable 中的查询运算符。此方法在简化某些类型的本地查询(尤其是使用 LINQ to XML 时)方面非常有效,第 对此进行了演示。但是,它不适用于基于 IQueryable<T> 的序列(例如,在查询数据库时),因为 Queryable 中的运算符需要 lambda 表达式才能发出表达式树。我们将在后面中讨论这个问题。

自然排序

输入序列中元素的原始顺序在 LINQ 中非常重要。某些查询运算符依赖于此排序,例如 Take 、 跳过 和 反向 。

Take 运算符输出前 x 个元素,丢弃其余元素:

int[] numbers  = { 10, 9, 8, 7, 6 };IEnumerable<int> firstThree = numbers.Take (3);     // { 10, 9, 8 }

Skip 运算符忽略前 x 个元素并输出其余元素:

IEnumerable<int> lastTwo    = numbers.Skip (3);     // { 7, 6 }

反向完全按照它所说的那样做:

IEnumerable<int> reversed   = numbers.Reverse();    // { 6, 7, 8, 9, 10 }

对于本地查询(LINQ 到对象),“位置”和“选择”等运算符将保留输入序列的原始顺序(与所有其他查询运算符一样,专门更改顺序的运算符除外)。

其他运营商

并非所有查询运算符都返回序列。元素运算符从输入序列中提取一个;例如 First 、 Last 和 ElementAt :

int[] numbers    = { 10, 9, 8, 7, 6 };int firstNumber  = numbers.First();                        // 10int lastNumber   = numbers.Last();                         // 6int secondNumber = numbers.ElementAt(1);                   // 9int secondLowest = numbers.OrderBy(n=>n).Skip(1).First();  // 7

由于这些运算符返回单个元素,因此通常不会对其结果调用进一步的查询运算符,除非该元素本身是集合。

运算符返回标量值,通常为数字类型:

int count = numbers.Count();          // 5;int min = numbers.Min();              // 6;

返回一个布尔值:

bool hasTheNumberNine = numbers.Contains (9);          // truebool hasMoreThanZeroElements = numbers.Any();          // truebool hasAnOddElement = numbers.Any (n => n % 2 != 0);  // true

某些查询运算符接受两个输入序列。例如 Concat ,它将一个序列附加到另一个序列,以及 Union ,它执行相同的操作,但删除了重复项:

int[] seq1 = { 1, 2, 3 };int[] seq2 = { 3, 4, 5 };IEnumerable<int> concat = seq1.Concat (seq2);    //  { 1, 2, 3, 3, 4, 5 }IEnumerable<int> union  = seq1.Union (seq2);     //  { 1, 2, 3, 4, 5 }

连接运算符也属于这一类。详细介绍了所有查询运算符。

查询表达式

C# 提供了用于编写 LINQ 查询的语法快捷方式,称为。与流行的看法相反,查询表达式不是将 SQL 嵌入到 C# 中的方法。事实上,查询表达式的设计主要受到函数式编程语言(如LISP和Haskell)的列表的启发,尽管SQL具有表面影响。

注意

在本书中,我们将查询表达式语法简单地称为。

在上一节中,我们编写了一个流畅的语法查询来提取包含字母“a”的字符串,按长度排序并转换为大写。查询语法中的内容相同:

using System;using System.Collections.Generic;using System.Linq;string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };IEnumerable<string> query =  from    n in names  where   n.Contains ("a")     // Filter elements  orderby n.Length             // Sort elements  select  n.ToUpper();         // Translate each element (project)foreach (string name in query) Console.WriteLine (name);JAYMARYHARRY

查询表达式始终以 from 子句开头,以 select 或 group 子句结尾。from 子句声明了一个(在本例中为 n ),您可以将其视为遍历输入序列 — 更像 foreach 。图 说明了作为铁路图的完整语法。

注意

要阅读此图,请从左侧开始,然后像火车一样沿着轨道前进。例如,在必需的 from 子句之后,可以选择包含 orderby 、where 、let 或 join 子句。之后,您可以继续使用选择或组子句,也可以返回并包含来自 、orderby 、where 、let 或 join 子句的另一个子句。

查询语法

编译器通过将查询表达式转换为流畅语法来处理查询表达式。它以一种相当机械的方式做到这一点——就像它将foreach语句转换为对GetEnumerator和MoveNext的调用一样。这意味着你可以用查询语法编写的任何内容也可以用流畅的语法编写。编译器(最初)将我们的示例查询转换为以下内容:

IEnumerable<string> query = names.Where   (n => n.Contains ("a"))                                 .OrderBy (n => n.Length)                                 .Select  (n => n.ToUpper());

然后,Where 、OrderBy 和 Select 运算符使用与查询以流畅语法编写时适用的相同规则进行解析。在这种情况下,它们绑定到 Enumerable 类中的扩展方法,因为导入了 System.Linq 命名空间,并且名称实现了 IEnumerable<string> 。但是,在翻译查询表达式时,编译器并不特别支持枚举类。您可以将编译器视为机械地将单词“Where”、“OrderBy”和“Select”注入到语句中,然后编译它,就像您自己键入方法名称一样。这为它们的解决方式提供了灵活性。例如,我们在后面部分中编写的数据库查询中的运算符将绑定到 Queryable 中的扩展方法。

注意

如果我们从程序中删除使用 System.Linq 指令,查询将无法编译,因为 Where 、OrderBy 和 Select 方法将无处绑定。查询表达式无法编译,除非您导入 System.Linq 或具有这些查询方法实现的其他命名空间。

范围变量

紧跟在 from 关键字语法后面的标识符称为。范围变量是指要对其执行操作的序列中的当前元素。

在我们的示例中,范围变量 n 出现在查询的每个子句中。然而,该变量实际上使用每个子句枚举序列:

from    n in names           // n is our range variablewhere   n.Contains ("a")     // n = directly from the arrayorderby n.Length             // n = subsequent to being filteredselect  n.ToUpper()          // n = subsequent to being sorted

当我们检查编译器对流畅语法的机械翻译时,这一点变得很清楚:

names.Where   (n => n.Contains ("a"))      // Locally scoped n     .OrderBy (n => n.Length)              // Locally scoped n     .Select  (n => n.ToUpper())           // Locally scoped n

如您所见,n 的每个实例都私下限定为其自己的 lambda 表达式。

查询表达式还允许您通过以下引入新的范围变量:

让到附加从条款加入

我们将在本章后面的“”以及的“”和中介绍这些内容。

查询语法与 SQL 语法

查询表达式表面上看起来类似于 SQL,但两者却大不相同。LINQ 查询归结为 C# 表达式,因此遵循标准 C# 规则。例如,对于 LINQ,在声明变量之前不能使用该变量。在 SQL 中,可以在 SELECT 子句中引用表别名,然后再在 FROM 子句中定义表别名。

LINQ 中的子查询只是另一个 C# 表达式,因此不需要特殊语法。SQL 中的子查询受特殊规则的约束。

使用 LINQ,数据在逻辑上从左到右流经查询。对于 SQL,在数据流方面的顺序结构不太好。

LINQ 查询由传送带或运算符管道组成,这些接受和发出元素顺序可能很重要的序列。SQL 查询由一个子句组成,这些子句主要处理。

查询语法与流畅语法

查询和流畅语法各有优势。

对于涉及以下任何一项的查询,查询语法更简单:

一个 let 子句,用于在范围变量旁边引入新变量选择多个 、 联接 或 组联接 ,后跟外部范围变量引用

(我们在中描述了let子句;我们在中描述了SelectMany,join和GroupJoin。

中间地带是涉及简单使用 Where 、OrderBy 和 Select 的查询。这两种语法都很好用;这里的选择主要是个人的。

对于包含单个运算符的查询,流畅的语法更短且不。

最后,有许多运算符在查询语法中没有关键字。这些要求您使用流畅的语法 - 至少部分使用。这意味着以下以外的任何运算符:

Where, Select, SelectManyOrderBy, ThenBy, OrderByDescending, ThenByDescendingGroupBy, Join, GroupJoin
混合语法查询

如果查询运算符不支持查询语法,则可以混合使用查询语法和流畅语法。唯一的限制是每个查询语法组件必须是完整的(即,以 from 子句开头,以 select 或 group 子句结尾)。

假设此数组声明

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };

以下示例计算包含字母“A”的名称数:

int matches = (from n in names where n.Contains ("a") select n).Count();// 3

下一个查询按字母顺序获取名字:

string first = (from n in names orderby n select n).First();   // Dick

混合语法方法有时在更复杂的查询中很有用。但是,通过这些简单的例子,我们可以始终坚持流畅的语法而不会受到惩罚:

int matches = names.Where (n => n.Contains ("a")).Count();   // 3string first = names.OrderBy (n => n).First();               // Dick
注意

有时,混合语法查询在功能和简单性方面提供了迄今为止最高的“物有所值”。重要的是不要单方面偏爱查询或流畅语法;否则,您将无法编写混合语法查询,而它们是最佳选择。

在适用的情况下,本章的其余部分显示了流畅语法和查询语法中的关键概念。

延迟执行

大多数查询运算符的一个重要特征是,它们不是在构造时执行,而是在时执行(换句话说,当在其枚举器上调用 MoveNext 时)。请考虑以下查询:

var numbers = new List<int> { 1 };IEnumerable<int> query = numbers.Select (n => n * 10);    // Build querynumbers.Add (2);                    // Sneak in an extra elementforeach (int n in query)  Console.Write (n + "|");          // 10|20|

我们在构造查询潜入列表的额外数字包含在结果中,因为直到 foreach 语句运行才会进行任何筛选或排序。这称为执行或执行,与委托发生的情况相同:

Action a = () => Console.WriteLine ("Foo");// We’ve not written anything to the Console yet. Now let’s run it:a();  // Deferred execution!

所有标准查询运算符都提供延迟执行,但以下:

返回单个元素或标量值(如 First 或 Count)的运算符以下:ToArray, ToList, ToDictionary, ToLookup, ToHashSet

这些运算符会导致立即执行查询,因为它们的结果类型没有提供延迟执行的机制。例如,Count 方法返回一个简单的整数,然后不会枚举该整数。将立即执行以下查询:

int matches = numbers.Where (n => n <= 2).Count();    // 1

延迟执行很重要,因为它将查询与查询分离。这允许您通过多个步骤构造查询,并使数据库查询成为可能。

注意

子查询提供另一个级别的间接寻址。子查询中的所有内容都受到延迟执行的影响,包括聚合和转换方法。我们在中对此进行了描述。

延迟执行还有另一个后果:重新枚举时,将重新评估延迟执行查询:

var numbers = new List<int>() { 1, 2 };IEnumerable<int> query = numbers.Select (n => n * 10);foreach (int n in query) Console.Write (n + "|");   // 10|20|numbers.Clear();foreach (int n in query) Console.Write (n + "|");   // <nothing>

重新评估有时不利有几个原因:

有时,您希望在某个时间点“冻结”或缓存结果。某些查询是计算密集型的(或依赖于查询远程数据库),因此您不希望不必要地重复它们。

您可以通过调用转换运算符(如 ToArray 或 ToList)来击败重新评估。ToArray 将查询的输出复制到数组;ToList 复制到通用列表<T> :

var numbers = new List<int>() { 1, 2 };List<int> timesTen = numbers  .Select (n => n * 10)  .ToList();                // Executes immediately into a List<int>numbers.Clear();Console.WriteLine (timesTen.Count);      // Still 2
捕获的变量

如果查询的 lambda 表达式外部变量,则查询将在查询遵循这些变量的值:

int[] numbers = { 1, 2 };int factor = 10;IEnumerable<int> query = numbers.Select (n => n * factor);factor = 20;foreach (int n in query) Console.Write (n + "|");   // 20|40|

在 for 循环中构建查询时,这可能是一个陷阱。例如,假设我们要从字符串中删除所有元音。以下方法虽然效率低下,但给出了正确的结果:

IEnumerable<char> query = "Not what you might expect";query = query.Where (c => c != 'a');query = query.Where (c => c != 'e');query = query.Where (c => c != 'i');query = query.Where (c => c != 'o');query = query.Where (c => c != 'u');foreach (char c in query) Console.Write (c);  // Nt wht y mght xpct

现在看看当我们用 for 循环重构它时会发生什么:

IEnumerable<char> query = "Not what you might expect";string vowels = "aeiou";for (int i = 0; i < vowels.Length; i++)  query = query.Where (c => c != vowels[i]);foreach (char c in query) Console.Write (c);

枚举查询时会抛出 IndexOutOfRangeException,因为正如我们在第 中看到的(参见),编译器在 for 循环中限定迭代变量的范围,就好像它是声明的一样。因此,当查询实际枚举时,每个闭包都会捕获变量 (i),其值为 5。要解决此问题,必须将循环变量分配给语句块声明的另一个变量:

for (int i = 0; i < vowels.Length; i++){  char vowel = vowels[i];  query = query.Where (c => c != vowel);}

这会强制在每次循环迭代中捕获新的局部变量。

注意

解决此问题的另一种方法是将 for 循环替换为 foreach 循环:

foreach (char vowel in vowels)  query = query.Where (c => c != vowel);
延迟执行的工作原理

查询运算符通过返回序列来提供延迟执行。

与传统的集合类(如数组或链表)不同,装饰器序列(通常)没有自己的支持结构来存储元素。相反,它包装了您在运行时提供的另一个序列,并维护对该序列的永久依赖关系。每当从装饰器请求数据时,它又必须从包装的输入序列请求数据。

注意

查询运算符的转换构成了“修饰”。如果输出序列未执行转换,它将是而不是装饰器。

调用 Where 仅构造装饰器包装器序列,该序列包含对输入序列、lambda 表达式和提供的任何其他参数的引用。仅当枚举装饰器时,才会枚举输入序列。

说明了以下查询的组合:

IEnumerable<int> lessThanTen = new int[] { 5, 12, 3 }.Where (n => n < 10);

装饰器序列

当你枚举 lessThanTen 时,你实际上是通过 Where 装饰器查询数组。

好消息(如果你想要编写自己的查询运算符)是,使用 C# 迭代器实现装饰器序列很容易。下面介绍如何编写自己的 Select 方法:

public static IEnumerable<TResult> MySelect<TSource,TResult>  (this IEnumerable<TSource> source, Func<TSource,TResult> selector){  foreach (TSource element in source)    yield return selector (element);}

此方法是借助收益返回语句的迭代器。从功能上讲,它是以下各项的快捷方式:

public static IEnumerable<TResult> MySelect<TSource,TResult>  (this IEnumerable<TSource> source, Func<TSource,TResult> selector){  return new SelectSequence (source, selector);}

其中 是一个(编译器编写的)类,其枚举器将逻辑封装在迭代器方法中。SelectSequence

因此,当您调用诸如 Select 或 Where 之类的运算符时,您所做的只是实例化一个装饰输入序列的可枚举类。

链接装饰器

链接查询运算符会创建修饰器的分层。请考虑以下查询:

IEnumerable<int> query = new int[] { 5, 12, 3 }.Where   (n => n < 10)                                               .OrderBy (n => n)                                               .Select  (n => n * 10);

每个查询运算符实例化一个新的装饰器,该装饰器包装了前一个序列(更像俄罗斯嵌套娃娃)。 说明了此查询的对象模型。请注意,此对象模型是在进行任何枚举之前完全构造的。

分层装饰器序列

枚举查询时,您正在查询通过分层或修饰器链转换的原始数组。

注意

将 ToList 添加到此查询的末尾将导致前面的运算符立即执行,从而将整个对象模型折叠到单个列表中。

显示了统一建模语言 (UML) 语法中的相同对象组合。Select 的修饰器引用 OrderBy 装饰器,该修饰器引用 Where 的修饰器,后者引用数组。延迟执行的一个功能是,如果逐步编写查询,则可以生成相同的对象模型:

IEnumerable<int>  source    = new int[] { 5, 12, 3 },  filtered  = source   .Where   (n => n < 10),  sorted    = filtered .OrderBy (n => n),  query     = sorted   .Select  (n => n * 10);

UML 装饰器组成

如何执行查询

下面是枚举上述查询的结果:

foreach (int n in query) Console.WriteLine (n);3050

在幕后,foreach 在 Select 的装饰器(最后一个或最外层的运算符)上调用 GetEnumerator,这将启动一切。结果是一个枚举器链,它在结构上镜像装饰器序列链。 说明了枚举过程中的执行流。

执行本地查询

在本章的第一部分中,我们将查询描述为传送带生产线。扩展这个类比,我们可以说 LINQ 查询是一条惰性生产线,其中传送带仅滚动元素。构造查询会构建一条生产线 — 一切就绪 — 但没有任何滚动。然后,当使用者请求元素(枚举查询)时,最右边的传送带激活;这反过来又会触发其他元素滚动 - 当需要输入序列元素时。LINQ 遵循需求驱动的模型,而不是供应驱动的模型。正如稍后将看到的那样,这对于允许 LINQ 扩展到查询 SQL 数据库非常重要。

子查询

子查询是包含在另一个的 lambda 表达式中的查询。下面的示例使用子查询按姓氏对音乐家进行排序:

string[] musos =   { "David Gilmour", "Roger Waters", "Rick Wright", "Nick Mason" };IEnumerable<string> query = musos.OrderBy (m => m.Split().Last());

m.Split 将每个字符串转换为单词集合,然后我们在此基础上调用 Last 查询运算符。m.Split().最后一个是子查询;查询引用。

允许使用子查询,因为您可以将任何有效的 C# 表达式放在 lambda 的右侧。子查询只是另一个 C# 表达式。这意味着子查询的规则是 lambda 表达式规则(以及查询运算符的一般行为)的结果。

注意

术语在一般意义上具有更广泛的含义。为了描述 LINQ,我们仅将该术语用于从另一个查询的 lambda 表达式中引用的查询。在查询表达式中,子查询相当于从除 from 子句之外的任何子句中的表达式引用的查询。

子查询私有作用域为封闭表达式,并且可以引用外部 lambda 表达式中的参数(或查询中的范围变量)。

m.Split().最后一个是一个非常简单的子查询。下一个查询检索数组中长度与最短字符串长度匹配的所有字符串:

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };IEnumerable<string> outerQuery = names  .Where (n => n.Length == names.OrderBy (n2 => n2.Length)                                .Select  (n2 => n2.Length).First());// Tom, Jay

下面是与查询表达式相同的内容:

IEnumerable<string> outerQuery =  from   n in names  where  n.Length ==           (from n2 in names orderby n2.Length select n2.Length).First()  select n;

因为外部范围变量 ( n ) 在子查询的范围内,我们不能重用 n 作为子查询的范围变量。

每当计算封闭的 lambda 表达式时,都会执行子查询。这意味着子查询由外部查询自行决定按需执行。你可以说执行是从进行的。本地查询从字面上遵循此模型;解释查询(例如,数据库查询)在遵循此模型。

子查询在需要时执行,以馈送外部查询。如图 和图 8-8 所示,我们示例中的子查询( 中的顶部传送带)对每个外部循环迭代执行一次。

子查询组合

我们可以更简洁地表达前面的子查询,如下所示:

IEnumerable<string> query =  from   n in names  where  n.Length == names.OrderBy (n2 => n2.Length).First().Length  select n;

使用 Min 聚合函数,我们可以进一步简化查询:

IEnumerable<string> query =  from   n in names  where  n.Length == names.Min (n2 => n2.Length)  select n;

在中,我们描述了如何查询远程源(如 SQL 表)。我们的示例是一个理想的数据库查询,因为它将作为一个单元进行处理,只需要一次往返数据库服务器。但是,此查询对于本地集合效率低下,因为子查询在每次外部循环迭代时都会重新计算。我们可以通过单独运行子查询来避免这种低效率(这样它就不再是子查询):

int shortest = names.Min (n => n.Length);IEnumerable<string> query = from   n in names                            where  n.Length == shortest                            select n;

UML 子查询组合注意

查询本地集合时,几乎总是需要以这种方式分解子查询。例外情况是当子,这意味着它引用外部范围变量。我们将在中探索相关的子查询。

子查询和延迟执行

子查询中的元素或聚合运算符(如 First 或 Count)不会强制外部查询立即执行 - 延迟执行仍适用于查询。这是因为子查询是调用的 - 对于本地查询,则通过委托调用,对于解释查询,则通过表达式树调用。

当您在 Select 表达式中包含子查询时,会出现一个有趣的情况。对于本地查询,您实际上是 — 每个查询本身都会延迟执行。效果通常是透明的,有助于进一步提高效率。我们将在第 中详细地重新讨论“选择子查询”。

构图策略

在本节中,我们将介绍构建更复杂查询的三种策略:

渐进式查询构造使用 into 关键字包装查询

所有这些都是策略,并生成相同的运行时查询。

渐进式查询构建

在本章的开头,我们演示了如何逐步构建流畅的查询:

var filtered   = names    .Where   (n => n.Contains ("a"));var sorted     = filtered .OrderBy (n => n);var query      = sorted   .Select  (n => n.ToUpper());

由于每个参与的查询运算符都返回一个修饰器序列,因此生成的查询与从单表达式查询获取的修饰器链或分层相同。但是,逐步构建查询有几个潜在的好处:

它可以使查询更易于编写。您可以添加查询运算符。例如if (includeFilter) query = query.Where (...)比query = query.Where (n => !includeFilter || <expression>)因为它避免在 includeFilter 为 false 时添加额外的查询运算符。

渐进式方法在查询推导中通常很有用。想象一下,我们要从名称列表中删除所有元音,然后按字母顺序显示长度仍超过两个字符的元音。在流畅的语法中,我们可以将此查询编写为单个表达式 - 通过在过滤投影:

IEnumerable<string> query = names  .Select  (n => n.Replace ("a", "").Replace ("e", "").Replace ("i", "")                  .Replace ("o", "").Replace ("u", ""))  .Where   (n => n.Length > 2)  .OrderBy (n => n);// Dck// Hrry// Mry
注意

与其调用字符串的 Replace 方法五次,我们可以使用正则表达式更有效地从字符串中删除元音:

n => Regex.Replace (n, "[aeiou]", "")

不过,string 的 Replace 方法的优点是也适用于数据库查询。

将其直接转换为查询表达式很麻烦,因为 select 子句必须位于 where 和 orderby 子句之后。如果我们重新排列查询以便最后投影,结果会有所不同:

IEnumerable<string> query =  from    n in names  where   n.Length > 2  orderby n  select  n.Replace ("a", "").Replace ("e", "").Replace ("i", "")           .Replace ("o", "").Replace ("u", "");// Dck// Hrry// Jy// Mry// Tm

幸运的是,有多种方法可以在查询语法中获取原始结果。第一种是通过逐步查询:

IEnumerable<string> query =  from   n in names  select n.Replace ("a", "").Replace ("e", "").Replace ("i", "")          .Replace ("o", "").Replace ("u", "");query = from n in query where n.Length > 2 orderby n select n;// Dck// Hrry// Mry
输入关键字注意

查询表达式以两种非常不同的方式解释 into 关键字,具体取决于上下文。我们现在描述的含义是向发出信号(另一个是向 GroupJoin 发出信号)。

into 关键字允许您在投影后“继续”查询,并且是渐进式查询的快捷方式。使用 into ,我们可以重写前面的查询,如下所示:

IEnumerable<string> query =  from   n in names  select n.Replace ("a", "").Replace ("e", "").Replace ("i", "")          .Replace ("o", "").Replace ("u", "")  into noVowel    where noVowel.Length > 2 orderby noVowel select noVowel;

唯一可以使用 into 的地方是在选择或组子句之后。进入“重新启动”查询,允许您引入新的 Where 、orderby 和 select 子句。

注意

尽管从查询表达式的角度来看,最容易将其视为重新启动查询,但当转换为最终的流畅形式时,它。因此,没有内在性能的打击。您也不会因使用它而损失任何积分!

在流畅的语法中,相当于 into 只是一个更长的运算符链。

范围规则

所有范围变量都超出 into 关键字后的范围。以下内容不会编译:

var query =  from n1 in names  select n1.ToUpper()  into n2                              // Only n2 is visible from here on.    where n1.Contains ("x")            // Illegal: n1 is not in scope.    select n2;

要了解原因,请考虑如何映射到流畅的语法:

var query = names  .Select (n1 => n1.ToUpper())  .Where  (n2 => n1.Contains ("x"));     // Error: n1 no longer in scope

原始名称 (n1) 在 Where 过滤器运行时丢失。其中 的输入序列仅包含大写名称,因此无法基于 n1 进行过滤。

包装查询

逐步构建的查询可以通过将一个查询包裹在另一个查询周围来表述为单个语句。一般而言

var tempQuery = tempQueryExprvar finalQuery = from ... in tempQuery ...

可以重新表述为:

var finalQuery = from ... in (tempQueryExpr)

包装在语义上与渐进式查询构建或使用 into 关键字(没有中间变量)相同。在所有情况下,最终结果都是查询运算符的线性链。例如,请考虑以下查询:

IEnumerable<string> query =  from   n in names  select n.Replace ("a", "").Replace ("e", "").Replace ("i", "")          .Replace ("o", "").Replace ("u", "");query = from n in query where n.Length > 2 orderby n select n;

以包装形式重新配制,如下:

IEnumerable<string> query =  from n1 in  (    from   n2 in names    select n2.Replace ("a", "").Replace ("e", "").Replace ("i", "")             .Replace ("o", "").Replace ("u", "")  )  where n1.Length > 2 orderby n1 select n1;

转换为流畅语法时,结果是与前面示例中相同的线性运算符链:

IEnumerable<string> query = names  .Select  (n => n.Replace ("a", "").Replace ("e", "").Replace ("i", "")                  .Replace ("o", "").Replace ("u", ""))  .Where   (n => n.Length > 2)  .OrderBy (n => n);

(编译器不会发出最终的 .选择 (n => n) ,因为它是多余的。

包装的查询可能会令人困惑,因为它们类似于我们之前编写的。两者都具有内部和外部查询的概念。但是,当转换为流畅语法时,您可以看到包装只是顺序链接运算符的一种策略。最终结果与子查询没有任何相似之处,子查询将内部查询嵌入到另一个查询的 中。

回到前面的类比:包装时,“内部”查询相当于。相比之下,子查询位于传送带上方,并根据需要通过传送带的 lambda 工作线程激活( 所示)。

投影策略对象初始值设定项

到目前为止,我们所有的选择子句都有投影的标量元素类型。使用 C# 对象初始值设定项,可以投影到更复杂的类型中。例如,假设作为查询的第一步,我们希望从名称列表中去除元音,同时仍保留原始版本,以便于后续查询。我们可以编写以下类来提供帮助:

class TempProjectionItem{  public string Original;    // Original name  public string Vowelless;   // Vowel-stripped name}

然后,我们可以使用对象初始值设定项投影到其中:

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };IEnumerable<TempProjectionItem> temp =  from n in names  select new TempProjectionItem  {    Original  = n,    Vowelless = n.Replace ("a", "").Replace ("e", "").Replace ("i", "")                 .Replace ("o", "").Replace ("u", "")  };

结果的类型为 IEnumerable<TempProjectionItem> ,我们随后可以查询:

IEnumerable<string> query = from   item in temp                            where  item.Vowelless.Length > 2                            select item.Original;// Dick// Harry// Mary
匿名类型

匿名类型允许您在不编写特殊类的情况下构建中间结果。我们可以在前面的示例中使用匿名类型消除 TempProjectionItem 类:

var intermediate = from n in names  select new  {    Original = n,    Vowelless = n.Replace ("a", "").Replace ("e", "").Replace ("i", "")                 .Replace ("o", "").Replace ("u", "")  };IEnumerable<string> query = from   item in intermediate                            where  item.Vowelless.Length > 2                            select item.Original;

这给出了与前面的示例相同的结果,但不需要编写一次性类。编译器完成这项工作,而是生成一个临时类,其中包含与我们的投影结构匹配的字段。但是,这意味着中间查询具有以下类型:

IEnumerable <random-compiler-generated-name>

我们可以声明这种类型的变量的唯一方法是使用 var 关键字。在这种情况下,var 不仅仅是一个减少杂波的设备;这是必须的。

我们可以使用 into 关键字更简洁地编写整个查询:

var query = from n in names  select new  {     Original = n,     Vowelless = n.Replace ("a", "").Replace ("e", "").Replace ("i", "")                  .Replace ("o", "").Replace ("u", "")  }  into temp  where temp.Vowelless.Length > 2  select temp.Original;

查询表达式为编写此类查询提供了快捷方式:let 。

让关键字

let 关键字在范围变量旁边引入了一个新变量。

使用 let,我们可以编写一个查询来提取长度(不包括元音)超过两个字符的字符串,如下所示:

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };IEnumerable<string> query =  from n in names  let vowelless = n.Replace ("a", "").Replace ("e", "").Replace ("i", "")                   .Replace ("o", "").Replace ("u", "")  where vowelless.Length > 2  orderby vowelless  select n;       // Thanks to let, n is still in scope.

编译器通过投影到包含范围变量和新表达式变量的临时匿名类型来解析 let 子句。换句话说,编译器将此查询转换为前面的示例。

让我们完成两件事:

它将新元素与现有元素一起投射。它允许在查询中重复使用表达式而无需。

let 方法在这个例子中特别有利,因为它允许 select 子句投影原始名称 ( n ) 或其元音删除版本(元音)。

您可以在 where 语句之前或之后有任意数量的 let 语句(参见)。let 语句可以引用前面的 let 语句中引入的变量(受 into 子句施加的边界的约束)。让我们透明地所有现有变量。

let 表达式不需要计算为标量类型:例如,有时让它计算为子序列很有用。

解释查询

LINQ 提供两种并行体系结构:本地对象集合的查询和远程数据源的。到目前为止,我们已经研究了本地查询的体系结构,这些查询在实现 IEnumerable<T> 的集合上运行。本地查询解析为 Enumerable 类中的查询运算符(默认情况下),而这些运算符又解析为修饰器序列链。它们接受的委托(无论是以查询语法、流畅语法还是传统委托表示)都是中间语言 (IL) 代码的完全本地,就像任何其他 C# 方法一样。

相比之下,解释查询是。它们对实现 IQueryable<T> 的序列进行操作,并解析为 Queryable 类中的查询运算符,这些运算符发出在运行时解释的。例如,可以将这些表达式树转换为 SQL 查询,从而允许您使用 LINQ 查询数据库。

注意

Enumerable 中的查询运算符实际上可以使用 IQueryable<T> 序列。困难在于生成的查询始终在客户端本地执行。这就是在 Queryable 类中提供第二组查询运算符的原因。

若要编写解释查询,需要从公开类型为 IQueryable<T> 的序列的 API 开始。一个例子是Microsoft的(EF Core),它允许您查询各种数据库,包括SQL Server,Oracle,MySQL,PostgreSQL和SQLite。

还可以通过调用 AsQueryable 方法,围绕普通可枚举集合生成 IQueryable<T> 包装器。我们在中描述了 AsQueryable。

注意

IQueryable<T> 是 IEnumerable<T> 的扩展,具有用于构造表达式树的其他方法。大多数情况下,您可以忽略这些方法的细节;它们由运行时间接调用。更详细地介绍了 IQueryable<T>。

为了说明这一点,让我们在 SQL Server 中创建一个简单的客户表,并使用以下 SQL 脚本用几个名称填充它:

create table Customer(  ID int not null primary key,  Name varchar(30))insert Customer values (1, 'Tom')insert Customer values (2, 'Dick')insert Customer values (3, 'Harry')insert Customer values (4, 'Mary')insert Customer values (5, 'Jay')

有了这个表,我们可以用 C# 编写一个解释型 LINQ 查询,该查询使用 EF Core 检索名称中包含字母“a”的客户,如下所示:

using System;using System.Linq;using Microsoft.EntityFrameworkCore;using var dbContext = new NutshellContext();IQueryable<string> query = from c in dbContext.Customers  where   c.Name.Contains ("a")  orderby c.Name.Length  select  c.Name.ToUpper();foreach (string name in query) Console.WriteLine (name);public class Customer{  public int ID { get; set; }  public string Name { get; set; }}// We’ll explain the following class in more detail in the next section.public class NutshellContext : DbContext{  public virtual DbSet<Customer> Customers { get; set; }  protected override void OnConfiguring (DbContextOptionsBuilder builder)    => builder.UseSqlServer ("...connection string...");  protected override void OnModelCreating (ModelBuilder modelBuilder)    => modelBuilder.Entity<Customer>().ToTable ("Customer")                                      .HasKey (c => c.ID);}

EF Core 将此查询转换为以下 SQL:

SELECT UPPER([c].[Name])FROM [Customers] AS [c]WHERE CHARINDEX(N'a', [c].[Name]) > 0ORDER BY CAST(LEN([c].[Name]) AS int)

这是最终结果:

// JAY// MARY// HARRY
解释查询的工作原理

让我们来看看如何处理上述查询。

首先,编译器将查询语法转换为流畅语法。这与本地查询完全相同:

IQueryable<string> query = dbContext.customers                                    .Where   (n => n.Name.Contains ("a"))                                    .OrderBy (n => n.Name.Length)                                    .Select  (n => n.Name.ToUpper());

接下来,编译器解析查询运算符方法。下面是本地查询和解释查询的不同之处 - 解释查询解析为 Queryable 类而不是 Enumerable 类中的查询运算符。

要了解原因,我们需要查看 dbContext.Customers 变量,这是整个查询构建的源。dbContext.Customers 的类型为 DbSet<T>,它实现了 IQueryable<T>(IEnumerable<T> 的子类型)。这意味着编译器可以选择解析 Where : 它可以调用 Enumerable 中的扩展方法或 Queryable 中的以下扩展方法:

public static IQueryable<TSource> Where<TSource> (this  IQueryable<TSource> source, Expression <Func<TSource,bool>> predicate)

编译器选择 Queryable.Where,因为它的签名是。

Queryable.Where 接受包装在 Expression<TDelegate> 类型的谓词。这将指示编译器将提供的 lambda 表达式(换句话说,n=>n.Name.Contains(“a”))转换为而不是已编译的委托。表达式树是基于 System.Linq.Expressions 中的类型的对象模型,可以在运行时进行检查(以便 EF Core 以后可以将其转换为 SQL 语句)。

因为 Queryable.Where 也返回 IQueryable<T>,所以 OrderBy 和 Select 运算符也遵循相同的过程。 说明了最终结果。在阴影框中,有一个描述整个查询的,可以在运行时遍历。

解释型查询组合执行

解释查询遵循延迟执行模型,就像本地查询一样。这意味着在开始枚举查询之前不会生成 SQL 语句。此外,枚举同一查询两次会导致数据库被查询两次。

在后台,解释查询在执行方式上与本地查询不同。枚举解释的查询时,最外层的序列将运行一个遍历整个表达式树的程序,并将其作为一个单元进行处理。在我们的示例中,EF Core 将表达式树转换为 SQL 语句,然后执行该语句,并以序列形式生成结果。

注意

若要正常工作,EF Core 需要了解数据库的架构。它通过利用约定、代码属性和流畅的配置 API 来实现这一点。我们将在本章后面详细探讨这一点。

我们之前说过,LINQ 查询就像一条生产线。但是,当您枚举 IQueryable 传送带时,它不会像本地查询那样启动整条生产线。取而代之的是,只有 IQueryable 传送带启动,并带有一个特殊的枚举器,该枚举器调用生产经理。经理审查整个生产线,其中不是由编译的代码组成,而是由(方法调用表达式)组成,并将指令粘贴到它们的(表达式树)。然后,管理器遍历所有表达式,在本例中将它们转录到一张纸(SQL 语句)上,然后执行该纸,并将结果反馈给使用者。只有一条皮带转动;生产线的其余部分是一个空壳网络,只是为了描述需要做什么而存在的。

这有一些实际意义。例如,对于本地查询,您可以编写自己的查询方法(使用迭代器相当容易),然后使用它们来补充预定义的集合。对于远程查询,这很困难,甚至是不可取的。如果你写了一个接受IQueryable<T>的MyWhere扩展方法,就像把你自己的假人放进生产线上一样。生产经理不知道如何处理你的假人。即使在此阶段进行了干预,您的解决方案也会硬连线到特定提供程序(如 EF Core),并且无法与其他 IQueryable 实现一起使用。在 Queryable 中使用一组标准方法的部分好处是,它们定义了用于查询远程集合的标准表。一旦您尝试扩展词汇表,您就不再可互操作。

此模型的另一个结果是,即使您坚持使用标准方法,IQueryable 提供商也可能无法处理某些查询。EF Core 受数据库服务器功能的限制;某些 LINQ 查询没有 SQL 转换。如果您熟悉 SQL,那么您会对这些内容有很好的直觉,尽管有时您需要进行实验以查看导致运行时错误的原因;令人惊讶的是,什么!

组合解释查询和本地查询

查询可以包括解释运算符和本地运算符。典型的模式是将本地运算符放在,将解释的组件放在;换句话说,解释的查询为本地查询提供信息。此模式在查询数据库时非常有效。

例如,假设我们编写一个自定义扩展方法来配对集合中的字符串:

public static IEnumerable<string> Pair (this IEnumerable<string> source){  string firstHalf = null;  foreach (string element in source)    if (firstHalf == null)      firstHalf = element;    else    {      yield return firstHalf + ", " + element;      firstHalf = null;    }}

我们可以在混合使用 EF Core 和本地的查询中使用此扩展方法:

using var dbContext = new NutshellContext ();IEnumerable<string> q = dbContext.Customers  .Select (c => c.Name.ToUpper())  .OrderBy (n => n)  .Pair()                         // Local from this point on.  .Select ((n, i) => "Pair " + i.ToString() + " = " + n);foreach (string element in q) Console.WriteLine (element);// Pair 0 = DICK, HARRY// Pair 1 = JAY, MARY

因为 dbContext.Customers 是实现 IQueryable<T> 的类型,所以 Select 运算符解析为 Queryable.Select 。这将返回一个同样为 IQueryable<T> 类型的输出序列,因此 OrderBy 运算符类似地解析为 。但是下一个查询运算符 Pair 没有重载接受 IQueryable<T> — 只有不太具体的 IEnumerable<T> 。因此,它解析为我们的本地 Pair 方法,即将解释的查询包装在本地查询中。Pair 还返回 ,因此后面的 Select 解析为另一个本地运算符。

在 EF Core 端,生成的 SQL 语句等效于以下内容:

SELECT UPPER([c].[Name]) FROM [Customers] AS [c] ORDER BY UPPER([c].[Name])

其余工作在本地完成。实际上,我们最终得到一个本地查询(在外部),其源是解释查询(内部)。

可枚举

Enumerable。AsEnumerable 是所有查询运算符中最简单的一个。以下是它的完整定义:

public static IEnumerable<TSource> AsEnumerable<TSource>              (this IEnumerable<TSource> source){    return source;}

其目的是将 IQueryable<T> 序列强制转换为 IEnumerable<T>,强制后续查询运算符绑定到可枚举运算符而不是可查询运算符。这会导致查询的其余部分在本地执行。

为了说明这一点,假设我们在 SQL Server 中有一个 MedicalArticles 表,并希望使用 EF Core 检索摘要少于 100 个单词的所有有关流感的文章。对于后一个谓词,我们需要一个正则表达式:

Regex wordCounter = new Regex (@"\b(\w|[-'])+\b");using var dbContext = new NutshellContext ();var query = dbContext.MedicalArticles  .Where (article => article.Topic == "influenza" &&                     wordCounter.Matches (article.Abstract).Count < 100);

问题是 SQL Server 不支持正则表达式,因此 EF Core 将引发异常,抱怨查询无法转换为 SQL。我们可以通过两个步骤进行查询来解决此问题:首先通过 EF Core 查询检索有关流感的所有文章,然后筛选少于 100 字的摘要:

Regex wordCounter = new Regex (@"\b(\w|[-'])+\b");using var dbContext = new NutshellContext ();IEnumerable<MedicalArticle> efQuery = dbContext.MedicalArticles  .Where (article => article.Topic == "influenza");IEnumerable<MedicalArticle> localQuery = efQuery  .Where (article => wordCounter.Matches (article.Abstract).Count < 100);

由于 efQuery 的类型为 IEnumerable<MedicalArticle>,因此第二个查询绑定到本地查询运算符,强制该部分筛选在客户端上运行。

使用 AsEnumerable ,我们可以在单个查询中执行相同的操作:

Regex wordCounter = new Regex (@"\b(\w|[-'])+\b");using var dbContext = new NutshellContext ();var query = dbContext.MedicalArticles  .Where (article => article.Topic == "influenza")  .AsEnumerable()  .Where (article => wordCounter.Matches (article.Abstract).Count < 100);

调用 AsEnumerable 的替代方法是调用 ToArray 或 ToList 。AsEnumerable 的优点是它不会强制立即执行查询,也不会创建任何存储结构。

注意

将查询处理从数据库服务器移动到客户端可能会损害性能,尤其是在这意味着检索更多行时。解决示例的一种更有效(尽管更复杂)的方法是使用 SQL CLR 集成在实现正则表达式的数据库上公开一个函数。

我们将在第 中进一步演示组合的解释查询和局部查询。

EF Core

在本章和第 中,我们使用 EF Core 来演示解释的查询。现在让我们来看看这项技术的主要功能。

EF 核心实体类

EF Core 允许使用任何类来表示数据,只要它包含要查询的每个列的公共属性即可。

例如,我们可以定义以下实体类来查询和更新数据库中的 表:

public class Customer{  public int ID { get; set; }   public string Name { get; set; }}
数据库上下文

定义实体类后,下一步是子类化 DbContext 。该类的实例表示使用数据库的会话。通常,DbContext 子类将为模型中的每个实体包含一个 DbSet<T> 属性:

public class NutshellContext : DbContext{  public DbSet<Customer> Customers { get; set; }  ... properties for other tables ...}

DbContext 对象执行三项操作:

它充当生成 DbSet<> 对象(您可以查询)的工厂。它会跟踪您对实体所做的任何更改,以便您可以将其写回(请参阅)。它提供了可以覆盖以配置连接和模型的虚拟方法。配置连接

通过重写 OnConfigure 方法,可以指定数据库提供程序和连接字符串:

public class NutshellContext : DbContext{  ...  protected override void OnConfiguring (DbContextOptionsBuilder                                          optionsBuilder) =>    optionsBuilder.UseSqlServer       (@"Server=(local);Database=Nutshell;Trusted_Connection=True");}

在此示例中,连接字符串被指定为字符串文本。生产应用程序通常会从配置文件(如 )中检索它。

UseSqlServer 是在程序集中定义的扩展方法,该程序集是 NuGet 包的一部分。软件包可用于其他数据库提供商,包括Oracle,MySQL,PostgresSQL和SQLite。

注意

如果您使用的是 ASP.NET,则可以允许其依赖注入框架预配置选项生成器;在大多数情况下,这使您可以完全避免覆盖OnConfiguration。若要启用此功能,请在 DbContext 上定义一个构造函数,如下所示:

public NutshellContext (DbContextOptions<NutshellContext>                        options)  : base(options) { }

如果选择覆盖 OnConfiguration(如果 DbContext 用于其他方案,则可能提供配置),则可以检查是否已按如下方式配置选项:

protected override void OnConfiguring (  DbContextOptionsBuilder optionsBuilder){  if (!optionsBuilder.IsConfigured)  {    ...  }}

在 OnConfigure 方法中,您可以启用其他选项,包括延迟加载(请参阅)。

配置模型

默认情况下,EF Core 是基于,这意味着它从类和属性名称推断数据库架构。

您可以使用,方法是覆盖模型构建器参数上的 OnModelCreate 和调用扩展方法。例如,我们可以显式指定客户实体的数据库表名称,如下所示:

protected override void OnModelCreating (ModelBuilder modelBuilder) =>  modelBuilder.Entity<Customer>()    .ToTable ("Customer");   // Table is called 'Customer'

如果没有此代码,EF Core 会将此实体映射到名为“客户”而不是“客户”的表,因为我们在 DbContext 中有一个名为 Customers 的 DbSet<Customer> 属性:

public DbSet<Customer> Customers { get; set; }
注意

以下代码将所有实体映射到与实体类名(通常是单数)匹配的表,而不是与 DbSet<T> (通常为复数)匹配:

protected override void OnModelCreating (ModelBuilder                                        modelBuilder){  foreach (IMutableEntityType entityType in           modelBuilder.Model.GetEntityTypes())  {    modelBuilder.Entity (entityType.Name)      .ToTable (entityType.ClrType.Name);  }}

流畅的 API 提供了用于配置列的扩展语法。在下一个示例中,我们使用两种常用方法:

HasColumnName ,它将属性映射到不同名称的列是必需的,这表示列不可为空

protected override void OnModelCreating (ModelBuilder modelBuilder) =>  modelBuilder.Entity<Customer> (entity =>  {      entity.ToTable ("Customer");      entity.Property (e => e.Name)            .HasColumnName ("Full Name")  // Column name is 'Full Name'            .IsRequired();                // Column is not nullable  });

列出了 Fluent API 中一些最重要的方法。

注意

您可以通过将特殊属性应用于实体类和属性(“数据注释”)来配置模型,而不是使用流畅的 API。这种方法不太灵活,因为必须在编译时修复配置,并且不太强大,因为有些选项只能通过流畅的 API 进行配置。

流畅的 API 模型配置方法

方法

目的

到表

指定给定实体的数据库表名称

builder  .Entity<Customer>()  .ToTable("Customer");

有列名称

指定给定属性的列名称

builder.Entity<Customer>()  .Property(c => c.Name)  .HasColumnName("Full Name");

哈斯基

指定一个键(通常偏离约定)

builder.Entity<Customer>()  .HasKey(c => c.CustomerNr);

是必需的

指定属性需要一个值(不可为空)

builder.Entity<Customer>()  .Property(c => c.Name)  .IsRequired();

最大长度

指定宽度可以变化的可变长度类型(通常是字符串)的最大长度

builder.Entity<Customer>()  .Property(c => c.Name)  .HasMaxLength(60);

有列类型

指定列的数据库数据类型

builder.Entity<Purchase>()  .Property(p => p.Description)  .HasColumnType("varchar(80)");

忽视

忽略类型

builder.Ignore<Products>();

忽视

忽略类型的属性

builder.Entity<Customer>()  .Ignore(c => c.ChatName);

哈斯索引

指定应在数据库中用作索引的属性(或属性组合)

// Compound index:builder.Entity<Purchase>()  .HasIndex(p =>     new { p.Date, p.Price });// Unique index on one propertybuilder  .Entity<MedicalArticle>()  .HasIndex(a => a.Topic)  .IsUnique();

哈松

请参阅

builder.Entity<Purchase>()  .HasOne(p => p.Customer)  .WithMany(c => c.Purchases);

哈斯多

请参阅

builder.Entity<Customer>()  .HasMany(c => c.Purchases)  .WithOne(p => p.Customer);
创建数据库

EF Core 支持方法,这意味着可以从定义实体类开始,然后要求 EF Core 创建数据库。执行后者的最简单方法是在 DbContext 实例上调用以下方法:

dbContext.Database.EnsureCreated();

但是,更好的方法是使用 EF Core 的功能,该功能不仅创建数据库,还对其进行配置,以便 EF Core 将来可以在实体类更改时自动更新架构。可以在 Visual Studio 的程序包管理器控制台中启用迁移,并要求它使用以下命令创建数据库:

Install-Package Microsoft.EntityFrameworkCore.ToolsAdd-Migration InitialCreateUpdate-Database

第一个命令安装工具以从 Visual Studio 中管理 EF Core。第二个命令生成一个称为代码迁移的特殊 C# 类,其中包含创建数据库的说明。最后一个命令针对项目的应用程序配置文件中指定的数据库连接字符串运行这些指令。

使用 DbContext

定义实体类和子类化 DbContext 后,可以实例化 DbContext 并查询数据库,如下所示:

using var dbContext = new NutshellContext();Console.WriteLine (dbContext.Customers.Count());// Executes "SELECT COUNT(*) FROM [Customer] AS [c]"

您还可以使用 DbContext 实例写入数据库。下面的代码在 Customer 表中插入一行:

using var dbContext = new NutshellContext();Customer cust = new Customer(){  Name = "Sara Wells"};dbContext.Customers.Add (cust);dbContext.SaveChanges();    // Writes changes back to database

下面查询刚刚插入的客户数据库:

using var dbContext = new NutshellContext();Customer cust = dbContext.Customers  .Single (c => c.Name == "Sara Wells")

以下内容将更新该客户的名称并将更改写入数据库:

cust.Name = "Dr. Sara Wells";dbContext.SaveChanges();
注意

Single 运算符非常适合按主键检索行。与 First 不同的是,如果返回多个元素,它会抛出异常。

对象跟踪

DbContext 实例会跟踪它实例化的所有实体,因此只要您请求表中的相同行,它就可以将相同的实体反馈给您。换句话说,上下文在其生存期内永远不会发出两个引用表中同一行的单独实体(其中行由主键标识)。此功能称为。

为了说明这一点,假设名称按字母顺序排列的客户也具有最低的 ID。在下面的示例中,a 和 b 将引用同一个对象:

using var dbContext = new NutshellContext ();Customer a = dbContext.Customers.OrderBy (c => c.Name).First();Customer b = dbContext.Customers.OrderBy (c => c.ID).First();
释放数据库上下文

尽管 DbContext 实现了 IDisposable ,但您(通常)可以在不释放实例的情况下逃脱。释放会强制释放上下文的连接,但这通常是不必要的,因为只要完成从查询中检索结果,EF Core 就会自动关闭连接。

由于惰性计算,过早地处置上下文实际上可能会有问题。请考虑以下事项:

IQueryable<Customer> GetCustomers (string prefix){  using (var dbContext = new NutshellContext ())    return dbContext.Customers                    .Where (c => c.Name.StartsWith (prefix));}...foreach (Customer c in GetCustomers ("a"))  Console.WriteLine (c.Name);

这将失败,因为查询是在我们枚举查询时计算的,即在释放其 DbContext 。

但是,关于不处理上下文,有一些注意事项:

它依赖于连接对象释放 Close 方法上的所有非托管资源。尽管这适用于 SqlConnection,但理论上,如果您调用 Close 而不是 Dispose,第三方连接可以保持资源打开(尽管这可以说违反了 IDbConnection.Close 定义的合约)。如果在查询上手动调用 GetEnumerator(而不是使用 foreach ),然后无法释放枚举器或使用序列,则连接将保持打开状态。在这种情况下,释放 DbContext 可提供备份。有些人认为释放上下文(以及实现 IDisposable 的所有对象)更整洁。

如果要显式释放上下文,则必须将 DbContext 实例传递到 GetCustomers 等方法中,以避免上述问题。

在 ASP.NET 核心 MVC 等方案中,上下文实例通过依赖项注入 (DI) 提供,DI 基础结构将管理上下文生存期。它将在工作单元(例如在控制器中处理的 HTTP 请求)开始时创建,并在该工作单元结束时释放。

请考虑 EF Core 遇到第二个查询时会发生什么情况。它首先查询数据库并获取一行。然后,它读取此行的主键,并在上下文的实体缓存中执行查找。看到匹配项,它将返回现有对象。因此,如果另一个用户刚刚在数据库中更新了该客户的名称,则将忽略新值。这对于避免意外的副作用(Customer 对象可能在其他地方使用)以及管理并发性至关重要。如果您更改了 Customer 对象的属性,但尚未调用 SaveChanges ,则不希望自动覆盖属性。

注意

通过将 AsNoTracking 扩展方法链接到查询,或者将上下文中的 ChangeTracker.QueryTrackingBehavior 设置为 QueryTrackingBehavior.NoTracking 来禁用对象跟踪。当数据以只读方式使用时,无跟踪查询非常有用,因为它可以提高性能并减少内存使用。

若要从数据库中获取最新信息,必须实例化新上下文或调用 Reload 方法,如下所示:

dbContext.Entry (myCustomer).Reload();

最佳做法是在每个工作单元使用新的 DbContext 实例,这样就很少需要手动重新加载实体。

更改跟踪

更改通过 DbContext 加载的实体中的属性值时,EF Core 会识别更改,并在调用 SaveChanges 时相应地更新数据库。为此,它会创建通过 DbContext 子类加载的实体状态的快照,并在调用 SaveChanges 时(或手动查询更改跟踪时,稍后将看到)将当前状态与原始状态进行比较。可以在 DbContext 中枚举修订,如下所示:

foreach (var e in dbContext.ChangeTracker.Entries()){  Console.WriteLine ($"{e.Entity.GetType().FullName} is {e.State}");  foreach (var m in e.Members)    Console.WriteLine (      $"  {m.Metadata.Name}: '{m.CurrentValue}' modified: {m.IsModified}");}

调用 SaveChanges 时,EF Core 使用 ChangeTracker 中的信息构造 SQL 语句,这些语句将更新数据库以匹配对象中的更改,发出插入语句以添加新行,更新语句以修改数据,以及删除语句以删除从 DbContext 子类的对象图中移除的行。任何交易范围都适用;如果不存在,则将所有语句包装在新事务中。

您可以通过在实体中实现 INotifyPropertyChanged 和 INotifyPropertyChange 来优化更改跟踪。前者允许 EF Core 避免将修改后的实体与原始实体进行比较的开销;后者允许 EF Core 完全避免存储原始值。实现这些接口后,在配置模型时在模型构建器上调用 HasChangeTrackingStrategy 方法,以激活优化的更改跟踪。

导航属性

导航属性允许您执行以下操作:

查询相关表,无需手动联接插入、删除和更新相关行,而无需显式更新外键

例如,假设客户可以进行多次购买。我们可以表示和之间的一对多关系,包括以下实体:

public class Customer{  public int ID { get; set; }  public string Name { get; set; }  // Child navigation property, which must be of type ICollection<T>:  public virtual List<Purchase> Purchases {get;set;} = new List<Purchase>();}public class Purchase{          public int ID { get; set; }  public DateTime Date { get; set; }  public string Description { get; set; }  public decimal Price { get; set; }  public int CustomerID? { get; set; }     // Foreign key field  public Customer Customer { get; set; }   // Parent navigation property}

EF Core 能够从这些实体推断出 CustomerID 是表的外键,因为名称“CustomerID”遵循流行的命名约定。如果我们要求 EF Core 从这些实体创建数据库,它将在 Purchase.CustomerID 和 Customer.ID 之间创建外键约束。

注意

如果 EF Core 无法推断关系,则可以在 OnModelCreate 方法中显式配置它,如下所示:

modelBuilder.Entity<Purchase>()  .HasOne (e => e.Customer)  .WithMany (e => e.Purchases)  .HasForeignKey (e => e.CustomerID);

设置这些导航属性后,我们可以编写如下查询:

var customersWithPurchases = Customers.Where (c => c.Purchases.Any());

我们将在第 中详细介绍如何编写此类查询。

在导航集合中添加和删除实体

将新实体添加到集合导航属性时,EF Core 会在调用 SaveChanges 时自动填充外键:

Customer cust = dbContext.Customers.Single (c => c.ID == 1);Purchase p1 = new Purchase { Description="Bike",  Price=500 };Purchase p2 = new Purchase { Description="Tools", Price=100 };cust.Purchases.Add (p1);cust.Purchases.Add (p2);dbContext.SaveChanges();

在此示例中,EF Core 会自动将 1 写入每个新购买的 CustomerID 列,并将每个购买的数据库生成的 ID 写入“购买”。编号 .

从集合导航属性中删除实体并调用 SaveChanges 时,EF Core 将清除外键字段或从数据库中删除相应的行,具体取决于关系的配置或推断方式。在本例中,我们将 Purchase.CustomerID 定义为可为空的整数(以便我们可以表示没有客户的购买或现金交易),因此从客户中删除购买将清除其外键字段,而不是将其从数据库中删除。

加载导航属性

当 EF Core 填充实体时,它(默认情况下)不会填充其导航属性:

using var dbContext = new NutshellContext();var cust = dbContext.Customers.First();Console.WriteLine (cust.Purchases.Count);    // Always 0

一种解决方案是使用 Include 扩展方法,该方法指示 EF Core 导航属性:

var cust = dbContext.Customers   .Include (c => c.Purchases)  .Where (c => c.ID == 2).First();

另一种解决方案是使用投影。当您只需要使用某些实体属性时,此技术特别有用,因为它减少了数据传输:

var custInfo = dbContext.Customers   .Where (c => c.ID == 2)  .Select (c => new    {      Name = c.Name,      Purchases = c.Purchases.Select (p => new { p.Description, p.Price })    })  .First();

这两种技术都会通知 EF Core 需要哪些数据,以便可以在单个数据库查询中获取这些数据。还可以手动指示 EF Core 根据需要填充导航属性:

dbContext.Entry (cust).Collection (b => b.Purchases).Load();// cust.Purchases is now populated.

这称为。与上述方法不同,这会生成到数据库的额外往返行程。

延迟加载

加载导航属性的另一种方法称为。启用后,EF Core 会根据需要填充导航属性,方法是为每个实体类生成一个代理类,该代理类拦截访问卸载导航属性的尝试。为此,每个导航属性必须是虚拟的,并且在其中定义的类必须是可继承的(而不是密封的)。此外,发生延迟加载时,上下文必须未释放,以便可以执行其他数据库请求。

您可以在 DbContext 子类的 OnConfigure 方法中启用延迟装入,如下所示:

protected override void OnConfiguring (DbContextOptionsBuilder                                        optionsBuilder){  optionsBuilder    .UseLazyLoadingProxies()    ...}

(您还需要添加对 Microsoft.EntityFrameworkCore.Proxies NuGet 包的引用。

延迟加载的代价是,每次访问卸载的导航属性时,EF Core 都必须向数据库发出额外的请求。如果发出许多此类请求,则性能可能会因过度往返而受到影响。

注意

启用延迟加载后,类的运行时类型是从实体类派生的代理。例如:

using var dbContext = new NutshellContext();var cust = dbContext.Customers.First();  Console.WriteLine (cust.GetType());// Castle.Proxies.CustomerProxy
延迟执行

EF Core 查询受延迟执行的影响,就像本地查询一样。这允许您逐步构建查询。但是,在一个方面,EF Core 具有特殊的延迟执行语义,即当子查询出现在 Select 表达式中时。

使用本地查询,您可以获得双倍延迟执行,因为从功能角度来看,您正在选择一系列。因此,如果枚举外部结果序列但从不枚举内部序列,则子查询将永远不会执行。

使用 EF Core,子查询与主外部查询同时执行。这可以防止过度往返。

例如,以下查询在到达第一个 foreach 语句时在单个往返行程中执行:

using var dbContext = new NutshellContext ();var query = from c in dbContext.Customers            select               from p in c.Purchases               select new { c.Name, p.Price };foreach (var customerPurchaseResults in query)  foreach (var namePrice in customerPurchaseResults)    Console.WriteLine ($"{ namePrice.Name} spent { namePrice.Price}");

显式投影的任何导航属性都将在单个往返行程中完全填充:

var query = from c in dbContext.Customers            select new { c.Name, c.Purchases };foreach (var row in query)  foreach (Purchase p in row.Purchases)   // No extra round-tripping    Console.WriteLine (row.Name + " spent " + p.Price);

但是,如果我们在没有首先急切加载或投影的情况下枚举导航属性,则延迟执行规则适用。在以下示例中,EF Core 在每个循环迭代上执行另一个购买查询(假设启用了延迟加载):

foreach (Customer c in dbContext.Customers.ToArray())  foreach (Purchase p in c.Purchases)    // Another SQL round-trip    Console.WriteLine (c.Name + " spent " + p.Price);

当您想要基于只能在客户端上执行的测试有执行内部循环时,此模型是有利的:

foreach (Customer c in dbContext.Customers.ToArray())  if (myWebService.HasBadCreditHistory (c.ID))    foreach (Purchase p in c.Purchases)   // Another SQL round trip      Console.WriteLine (c.Name + " spent " + p.Price);
注意

请注意在前两个查询中使用了 ToArray。默认情况下,当当前查询的结果仍在处理中时,SQL Server 无法启动新查询。调用 ToArray 会具体化客户,以便可以发出其他查询来检索每个客户的购买情况。可以通过追加 ;MultipleActiveResultSets=True 到数据库连接字符串。请谨慎使用 MARS,因为它可以掩盖聊天的数据库设计,可以通过预先加载和/或投影所需数据来改进该设计。

(在第 中,我们将在中更详细地探讨选择子查询。

构建查询表达式

到目前为止,在本章中,当我们需要动态组合查询时,我们已经通过有条件地链接查询运算符来实现。尽管这在许多情况下已经足够了,但有时您需要在更精细的级别工作,并动态组合为运算符提供信息的 lambda 表达式。

在本节中,我们假设以下产品类别:

public class Product{  public int ID { get; set; }  public string Description { get; set; }  public bool Discontinued { get; set; }  public DateTime LastSale { get; set; }}
委托与表达式树

回想一下:

使用可枚举运算符的本地查询接受委托。使用可查询运算符的解释查询采用表达式树。

我们可以通过比较 Where 运算符在 Enumerable 和 Queryable 中的签名来看到这一点:

public static IEnumerable<TSource> Where<TSource> (this  IEnumerable<TSource> source, Func<TSource,bool> predicate)public static IQueryable<TSource> Where<TSource> (this  IQueryable<TSource> source, Expression<Func<TSource,bool>> predicate)

当嵌入到查询中时,lambda 表达式无论绑定到 Enumerable 的运算符还是 Queryable 的运算符,看起来都是一样的:

IEnumerable<Product> q1 = localProducts.Where (p => !p.Discontinued);IQueryable<Product>  q2 = sqlProducts.Where   (p => !p.Discontinued);

但是,当您将 lambda 表达式分配给中间变量时,您必须明确解析为委托(即 Func<> )还是表达式树(即表达式<Func<>> )。在下面的示例中,谓词 1 和谓词 2 不可互换:

Func <Product, bool> predicate1 = p => !p.Discontinued;IEnumerable<Product> q1 = localProducts.Where (predicate1);Expression <Func <Product, bool>> predicate2 = p => !p.Discontinued;IQueryable<Product> q2 = sqlProducts.Where (predicate2);
编译表达式树

可以通过调用 编译 将表达式树转换为委托。这在编写返回可重用表达式的方法时特别有价值。为了说明这一点,让我们向 Product 类添加一个静态方法,如果产品未停产且在过去 30 天内已售出,则返回一个计算结果为 true 的谓词:

public class Product{  public static Expression<Func<Product, bool>> IsSelling()  {    return p => !p.Discontinued && p.LastSale > DateTime.Now.AddDays (-30);  }}

刚刚编写的方法既可以在解释查询中使用查询中使用,如下所示:

void Test(){  var dbContext = new NutshellContext();  Product[] localProducts = dbContext.Products.ToArray();  IQueryable<Product> sqlQuery =    dbContext.Products.Where (Product.IsSelling());  IEnumerable<Product> localQuery =    localProducts.Where (Product.IsSelling().Compile());}
注意

.NET 不提供用于反向转换(从委托到表达式树)的 API。这使得表达式树更加通用。

AsQueryable

AsQueryable 运算符允许您编写可在本地或远程序列上运行的整个:

IQueryable<Product> FilterSortProducts (IQueryable<Product> input){  return from p in input         where ...         orderby ...         select p;}void Test(){  var dbContext = new NutshellContext();  Product[] localProducts = dbContext.Products.ToArray();  var sqlQuery   = FilterSortProducts (dbContext.Products);  var localQuery = FilterSortProducts (localProducts.AsQueryable());  ...}

AsQueryable 将 IQueryable<T> 服装包装在本地序列周围,以便后续查询运算符解析为表达式树。稍后枚举结果时,将隐式编译表达式树(性能成本较低),并且本地序列将像往常一样进行枚举。

表达式树

我们之前说过,从 lambda 表达式到 Expression<TDelegate> 的隐式转换会导致 C# 编译器发出生成表达式树的代码。通过一些编程工作,您可以在运行时手动执行相同的操作,换句话说,从头开始动态构建表达式树。结果可以强制转换为表达式< TDelegate>并在 EF Core 查询中使用,也可以通过调用 Compile 编译为普通委托。

表达式 DOM

表达式树是一个微型代码 DOM。树中的每个节点都由 System.Linq.Expressions 命名空间中的一个类型表示。 说明了这些类型。

表达式类型

所有节点的基类是(非泛型)表达式类。通用的 Expression<TDelegate> 类实际上意味着“类型化的 lambda 表达式”,如果不是因为这个笨拙>它可能被命名为 LambdaExpression<TDelegate:

LambdaExpression<Func<Customer,bool>> f = ...

Expression<T> 的基本类型是(非泛型)LambdaExpression 类。LamdbaExpression 为 lambda 表达式树提供了类型统一:任何类型化的 Expression<T> 都可以转换为 LambdaExpression。

LambdaExpression s 与普通表达式的区别在于 lambda 表达式具有。

若要创建表达式树,请不要直接实例化节点类型;而是调用 Expression 类上提供的静态方法,例如 Add 、And 、Call 、Constant 、LessThan 等。

显示了以下赋值创建的表达式树:

Expression<Func<string, bool>> f = s => s.Length < 5;

表达式树

我们可以按如下方式演示这一点:

Console.WriteLine (f.Body.NodeType);                     // LessThanConsole.WriteLine (((BinaryExpression) f.Body).Right);   // 5

现在让我们从头开始构建此表达式。原则是你从树的底部开始,然后一路向上。我们树中最底层的东西是一个参数表达式,字符串类型为“s”的lambda表达式参数:

ParameterExpression p = Expression.Parameter (typeof (string), "s");

下一步是构建 MemberExpression 和 ConstantExpression 。在前一种情况下,我们需要访问参数 “s” 的 Length :

MemberExpression stringLength = Expression.Property (p, "Length");ConstantExpression five = Expression.Constant (5);

接下来是小于比较:

BinaryExpression comparison = Expression.LessThan (stringLength, five);

最后一步是构造 lambda 表达式,该表达式将表达式 Body 链接到参数集合:

Expression<Func<string, bool>> lambda  = Expression.Lambda<Func<string, bool>> (comparison, p);

测试 lambda 的一种便捷方法是将其编译为委托:

Func<string, bool> runnable = lambda.Compile();Console.WriteLine (runnable ("kangaroo"));           // FalseConsole.WriteLine (runnable ("dog"));                // True
注意

确定要使用的表达式类型的最简单方法是在 Visual Studio 调试器中检查现有的 lambda 表达式。

标签: #c语言函数void fun寻找元音字母 #aspnetmvc3linq