第8章 LINQ 查询
8.2 流式语法
8.2.2 使用 Lambda 表达式
常用运算符
Where()
筛选器
Order()
排序器
Select()
映射器
Take()
获取前 x 个元素
Skip()
跳过前 x 个元素
Reverse()
反转所有元素
First()
获取第一个元素
Last()
获取最后一个元素
ElementAt()
获取第 x 个元素
Count()
获取元素数量
Min()
获取元素最小值
Contains()
查询是否包含某元素
Any()
查询是否包含指定条件的元素
Concat()
首尾拼接两个集合
Union()
求两个集合的并集(去重)
8.2.2.2 Lambda 表达式和元素类型
标准的查询运算符使用了以下的类型参数名称:
方向类型名称 | 含义 |
---|---|
TSource |
输入序列的元素类型 |
TResult |
输出序列的元素类型(如果和 TSource 不一致的话) |
TKey |
在排序、分组、连接操作中作为键的元素类型 |
8.3 查询表达式
查询表达式一般以 from
子句开始,最后以 select
或者 group
子句结束。
编译器在处理查询表达式前会将其翻译为流式语法形式,这意味着任何可以用查询语法完成的逻辑也可以用流式语法编写,以下两段代码等价:
string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };
IEnumerable<string> query =
from n in names
where n.Contains ("a")
orderby n.Length
select n.ToUpper();
query.Dump();
var names = new[] { "Tom", "Dick", "Harry", "Mary", "Jay" }.AsQueryable();
IEnumerable<string> query =
names.Where (n => n.Contains("a"))
.Order (n => n.Length)
.Select (n => n.ToUpper());
query.Dump();
Eureka
若删除了
using System.Linq
指令,查询表达式也将无法编译。因为编译器在编译 Where、OrderBy 和 Select 这些运算符时仍会绑定到与之对应的流式方法。
8.3.1 范围变量
在之前的例子中,每一个查询子句都使用了范围变量 n,并且不同子句的范围变量枚举的序列都是不同的:
string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" }; IEnumerable<string> query = from n in names where n.Contains ("a") orderby n.Length select n.ToUpper(); query.Dump();
var names = new[] { "Tom", "Dick", "Harry", "Mary", "Jay" }.AsQueryable(); IEnumerable<string> query = names.Where (n => n.Contains("a")) .Order (n => n.Length) .Select (n => n.ToUpper()); query.Dump();
正如流式语法那样,每个 n 只在当前范围内生效。以下的子句也可以在查询表达式中引入新的范围变量:
-
let
-
into
- 新的
from
子句 -
join
8.3.3 LINQ 查询语法与 SQL 语法的不同
C7.0 核心技术指南 第7版.pdf - p400 - C7.0 核心技术指南 第 7 版-P400-20240318224935
8.3.3 查询语法和流式语法 & 8.3.4 混合查询语法
查询语法仅支持如下:
Where
、Select
、SelectMany
、OrderBy
、ThenBy
、OrderByDescending
、ThenByDescending
、GroupBy
、Join
、GroupJoin
如果没有合适的查询语法,可以混用这两种语法:
(from n in names where n.Contains ("a") select n).Count()
.Dump ("Names containing the letter 'a'");
string first = (from n in names orderby n select n).First()
.Dump ("First name, alphabetically");
C7.0 核心技术指南 第7版.pdf - p401 - C7.0 核心技术指南 第 7 版-P401-20240318225533
8.4 延迟执行
几乎所有的标准查询运算符都具有延迟执行的能力,以下运算符除外:
- 返回 单个元素 或 标量值 的运算符,例如:
First
或Count
- 转换 运算符:
ToArray
、ToList
、ToDictionary
、ToLookup
上述运算符都会立即执行,因为其结果类型不具备任何延迟执行的机制(延迟执行需要结果是 可枚举 类型)。例如,
Count
方法返回一个整数,而整数是无法再枚举的。
8.4.1 重复执行
延迟执行的另一个后果是:当重复枚举时,延迟执行的查询也会重复执行。此时可以使用 转换 运算符,例如 ToArray
或 ToList
来避免重复执行。
8.4.2 捕获变量
如果 Lambda 捕获了外部变量,那么该变量的值将在表达式 执行 时决定。如下代码输出的值为“ 20 40 ”:
int[] numbers = { 1, 2 };
int factor = 10;
IEnumerable<int> query = numbers.Select (n => n * factor);
factor = 20;
query.Dump ();
尤其要注意 for 循环中对 索引 的误用:
for (int i = 0; i < vowels.Length; i++)
query = query.Where (c => c != vowels[i]);
C7.0 核心技术指南 第7版.pdf - p404 - C7.0 核心技术指南 第 7 版-P404-20240330165546-rwr1zcr
Warn
上述现象不仅限于 LINQ 和 Lambda 表达式,任何闭包都会有此问题!例如:
int value = 10; void DoSomething() { value.Dump(); } value = 20; // 输出 20 DoSomething();
8.6 构造方式
8.6.2 into
关键字
在查询表达式中我们提到:
查询表达式一般以
from
子句开始,最后以 select
或者 group
子句结束。
结束子句运行后将无法进行其他查询,此时我们可以使用 into
关键字:
var 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 关键字仅能出现在
select
和 group
子句之后
C7.0 核心技术指南 第7版.pdf - p413 - C7.0 核心技术指南 第 7 版-P413-20240330172956-yhz9dnh
into
的作用域
into 关键字后面的查询语句 不能 使用之前定义的范围变量,因此以下的查询是 无法 通过编译的:
var query =
from n1 in names
select n1.ToUpper()
into n2 // 后续语句仅有 n2 有效.
where n1.Contains ("x") // 非法: n1 已不在作用域.
select n2;
它相当于如下链式表达式:
var query = names
.Select (n1 => n1.ToUpper())
.Where (n2 => n1.Contains ("x")); // 错误: n1 已不在作用域
8.7 映射方式
8.7.1 对象初始化器 & 8.7.2 匿名类型
select 子句不但可以将结果映射为标量元素,也可以映射为类型实例,这个过程可以通过 对象初始化 器完成:
// 数据
var names = new[] { "Tom", "Dick", "Harry", "Mary", "Jay" }.AsQueryable();
IEnumerable<TempProjectionItem> temp =
from n in names
select new TempProjectionItem
{
Original = n,
Vowelless = n.Replace("a", "").Replace("e", "").Replace("i", "").Replace("o", "").Replace("u", "")
};
// 临时类型
class TempProjectionItem
{
public string Original; // Original name
public string Vowelless; // Vowel-stripped name
}
我们也可以使用匿名类型,省去类型定义:
var intermediate = from n in names
select new
{
Original = n,
Vowelless = n.Replace ("a", "").Replace ("e", "").Replace ("i", "").Replace ("o", "").Replace ("u", "")
};
8.7.3 let 关键字
在8.6.2 into 关键字中我们提到:
into 关键字后面的查询语句 不能 使用之前定义的范围变量,因此以下的查询是 无法 通过编译的:
let 关键字解决了这一问题:
var value = 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;
var value = 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
let 有两个方面的功能:
- 同时映射了 新 的元素和 已有 的元素。
- 允许在一个查询中无须重写而复用其中的表达式。
8.8 解释型查询
LINQ 提供了两种平行的架构:
本地查询 | 解释型查询 | |
---|---|---|
适用范围 | 本地对象集合 | 远程数据源 |
适用类型 | IEnumerable<T> 集合 |
IQuerable<T> 接口 |
工作方式 | 使用链式表达式,编译为 IL 代码 | 运行时解释,生成表达式树 |
IQuerable<T>
的实现方式有两种:
- LINQ to SQL
- Entity Framework(EF)
C7.0 核心技术指南 第7版.pdf - p418 - C7.0 核心技术指南 第 7 版-P418-20240405163532-1uikzy4
8.8.1 解释型查询的工作机制
以如下代码为例:
IQueryable<string> query = from c in Customers
where c.Name.Contains ("a")
orderby c.Name.Length
select c.Name.ToUpper();
1. 将查询语法转换为流式语法
这与本地查询相同,上述代码将转化为:
IQueryable<string> query = customers.Where (n => n.Name.Contains("a"))
.OrderBy (n => n.Name.Length)
.Select (n => n.Name.ToUpper());
2. 解析运算符方法
因 customer
(Table<T>
类型)实现了 IQueryable<T>
接口,因此上述方法将被解析为 IQueryable<T>
中的方法:
public static IQueryable<TSource> Where<TSource> (this
IQueryable<TSource> source, Expression <Func<TSource, bool>> predicate)
可以看到,Lambda 转换为 Expression
,而非 Func
委托。Expression
将进一步翻译为表达式树。
Tips
IQueryable<T>
接口继承了IEnumerable<T>
接口,不过 LINQ 对它们分别编写了不同的扩展方法。对比IEnumerable<T>
中的Where
:public static IEnumerable<TSource> Where<TSource> (this IEnumerable<TSource> source, Func<TSource, bool> predicate)
3. 执行
解释型查询会遵循延迟执行模式,表达式树在执行时会生成 SQL 语句。与本地查询不同。进行解释型查询时,会先遍历整个表达式树,并将其作为一个整体(一条 SQL 语句)来处理:
SELECT UPPER([to].[Name]) AS [value]
FROM [Customer] AS [to]
WHERE [to].[Name] LIKE @pO
ORDER BY LEN([to].[Name])
C7.0 核心技术指南 第7版.pdf - p420 - C7.0 核心技术指南 第 7 版-P420-20240405190210-48i40y3
8.8.2 混合使用解释型查询和本地查询
以如下代码为例,我们定义一个扩展方法,接受、返回 IEnumerable<string>
类型的对象,以此混合使用解释型查询和本地查询:
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;
}
}
}
Customers
.Select (c => c.Name.ToUpper())
.OrderBy (n => n)
.Pair() // 此处转为本地查询
.Select ((n, i) => "Pair " + i.ToString() + " = " + n)
.Dump();
-- 解释型查询将转化为如下 SQL 语句
SELECT UPPER (Name) FROM Customer ORDER BY UPPER (Name)
8.8.3 IEnumerable.AsEnumerable
扩展方法
该方法用于将 IQueryable<T>
转化为 IEnumerable<T>
,定义如下:
public static IEnumerable<TSource> AsEnumerable<TSource> (this IEnumerable<TSource> source) {
return source;
}
如下代码想在 LINQ 中使用正则表达式,而 SQL Server 不支持。我们可以将查询拆分为解释型和本地型两部分:
Regex wordCounter = new Regex (@"\b(\w|[-'])+\b");
// 如下查询将抛出异常
var query = MedicalArticles
.Where (article => article.Topic == "influenza"
&& wordCounter.Matches (article.Abstract).Count < 100);
var query = MedicalArticles
.Where (article => article.Topic == "influenza")
.AsEnumerable()
.Where (article => wordCounter.Matches (article.Abstract).Count < 100);
// 等价于
IEnumerable<MedicalArticle> sqlQuery = dataContext.MedicalArticles
.Where (article => article.Topic == "influenza")
IEnumerable<MedicalArticle> localQuery = sqlQuery
.Where (article => wordCounter.Matches (article.Abstract).Count < 100);
我们也可以使用
ToArray
或 ToList
达到同样的效果,不过AsEnumerable
可以延迟执行。
8.9 LINQ to SQL 和 Entity Framework
8.9.1 LINQ to SQL(L2S)的实体类 #delay# 用不到,等用到了再看
L2S 可以使用任何类表示数据,仅需通过 System.Data.Linq.Mapping
命名空间中的特性进行标记。常用的特性和参数有:
-
[Table]
用于 类-
Name
标记数据库 表 名
-
-
[Column]
用于 字段 或 属性-
IsPrimaryKey
标记是否为 主键 -
Name
标记 列 名 -
Storage
标记属性的 后备字段名称
-
例如:
[Table(Name = "Customers")]
public class Customer
{
[Column(IsPrimaryKey = true, Name = "FullName")]
public int Id;
private string _name;
[Column(Storage = nameof(_name))]
public string Name;
}
8.9.2 Entity Framework 的实体类 #delay# 当前未接触这部分内容,后面再制卡学习
对于 EF,需要 .edmx 文件,它包含三部分:
- 概念模型:描述了 EDM 模型,并对数据库进行了隔离
- 存储模型:描述了数据库的大纲(schema)
- 映射:定义了概念模型和存储模型的映射关系
通过 .edmx 文件可以创造复杂的映射关系:
- 将多个表映射为一个实体
- 将一个表映射为多个实体
- 使用 ORM 的三种标准方式将表映射为继承类型。
C7.0 核心技术指南 第7版.pdf - p426 - C7.0 核心技术指南 第 7 版-P426-20240405212628-2q9aj2f
以下是实体类的一个例子:
[EdmEntityType (NamespaceName ="NutshellModel", Name = "Customer")]
public partial class Customer{
[EdmScalarPropertyAttribute (EntityKeyProperty=true, IsNullable=false)]
public int ID {get;set;}
[EdmScalarProperty (EntityKeyProperty = false, IsNullable = false)]
public string Name {get; set;}
}
8.9.3 DataContext
和 ObjectContext
DataContext
(L2S)和 ObjectContext
(EF)用于查询(仅能用于 SQL Server),使用方式如下:
var l2sContext = new DataContext ("database connection string");
var efContext = new ObjectContext ("entity connection string");
其中,构造器传入数据库连接字符串。对于 ObjectContext
还要传入 .edmx 文件的访问方式。
获得上下文对象后,我们可以用它查询、更新数据库内容:
DataContext
使用示例
var context = new DataContext ("database connection string")
Table<Customer> customers = context.GetTable<Customer>();
// 查询
Console.WriteLine (customers.Count());
Customer cust = customers.Single (c => c.ID == 2);
// 修改
cust.Name = "Updated Name";
context.SubmitChanges();
ObjectContext
使用示例
var context = new ObjectContext ("entity connection string");
context.DefaultContainerName = "NutshellEntities";
ObjectSet<Customer> customers = context.CreateObjectSet<Customer>();
// 查询
Console.WriteLine (customers.Count());
Customer cust = customers.Single (c => c.ID == 2);
// 修改
cust.Name = "Updated Name";
context.SaveChanges();
8.9.3.1 类型化上下文
8.9.3 DataContext 和 ObjectContext 中的示例代码每次查询都需要调用 GetTable
或 CreateObjectSet
,并不方便。更好的方式是类型化上下文(typed context):
class Nutshellcontext : DataContext { // L2S
public Table<Customer> Customers => GetTable<Customer>();
}
class Nutshellcontext : ObjectContext { // EF
public Table<Customer> Customers => CreateObjectSet<Customer>();
}
调用可以得到极大简化:
var context = new Nutshellcontext("connection string");
Console.WriteLine (context.Customers.Count());
8.9.3.2 对象状态跟踪
DataContext
和 ObjectContext
实例会缓存所有查询结果,重复请求表中同一行时,它总会返回相同的实例。其工作方式如下:
新版 .NET 已不再支持这两个 Context,L2S 已被弃用。转而使用
DbContext
,原理与ObjectContext
相似。
8.9.4 关联 及后续内容
8.9 节这部分内容在新版 .NET 已被弃用,且主要面向 SQL Server,实用性不强,暂时不看
#delay#
8.10 构建查询表达式
本节后续例子我们都假定有如下 Product 类:
[Table] public partial class Product
{
[Column(IsPrimaryKey=true)] public int ID;
[Column] public string Description;
[Column] public bool Discontinued;
[Column] public DateTime LastSale;
}
8.10.1 委托与表达式树
在 2. 解析运算符方法有提到:
- 本地查询使用
Enumerable
运算符,接受 委托 。 - 解释型查询使用
Queryable
运算符,接受 表达式树 。
以如下代码为例,predicate1
和 predicate2
不能 互换:
// 委托
Func<Product, bool> predicate1 = p => !p.Discontinued;
// 表达式树
Expression<Func<Product, bool>> predicate2 = p => !p.Discontinued;
IEnumerable<Product> localProducts = ...;
IQueryable<Product> sqlProducts = ...;
var q1 = localProducts.Where(predicate1);
var q2 = sqlProducts.Where(predicate2);
实际上
sqlProducts.Where
可以传入predicate1
,不过这样它实际调用的是IEnumerable.Where
8.10.1.1 Expression<...>
转化为 Funk<...>
Expression<...>
的 Compile
方法可以见其转化为 Func<...>
:
var q1 = localProducts.Where(predicate2); // 无法编译
var q1 = localProducts.Where(predicate2.Compile());
8.10.1.2 AsQueryable
与 8.8.3 IEnumerable.AsEnumerable 扩展方法相似,可以将 IEnumerable
实例包装为 IQueryable
实例。
该方法主要用于提供兼容性:
IQueryable<Product> FilterSortProducts (IQueryable<Product> input)
{
return from p in input
where ...
order by ...
select p;
}
void Test()
{
var dataContext = new Nutshellcontext("connection string");
Product[] localProducts = dataContext.Products.ToArray();
var sqlQuery= FilterSortProducts(dataContext.Products);
var localQuery = FilterSortProducts(localProducts.AsQueryable());
...
}
8.10.2 表达式树
讲的太简略了,看不明白 #delay#
标签:Name,IEnumerable,LINQ,Replace,var,查询,Where From: https://www.cnblogs.com/hihaojie/p/18639814/chapter-8-linq-query-2p2sug