第9章 LINQ 运算符

本章所有例子所使用的 names 数组都是一致的:

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

9.1 概述

标准查询运算符分三类:

  1. 输入是序列(IEnumerable​),输出是序列(IEnumerable​)(IEnumerable​ → IEnumerable​)
  2. 输入是序列(IEnumerable​),输出是单个元素或标量值(IEnumerable​ → value)
  3. 没有输入,输出是序列(void → IEnumerable​)
mindmap
运算符分类
IEnumerable → IEnumerable
筛选
映射
连接
排序
分组
集合
转换
输入
输出
IEnumerable → value
提取元素
聚合
“断言”<br/>(量词)
void → IEnumerable
生成集合

9.1.1 序列 → 序列(IEnumerable​ → IEnumerable​)

9.1.1.1 筛选运算符

​IEnumerable<TSource>​ → IEnumerable<TSource>​

此类运算符返回原始序列的 子集 :

​ ​Where​ ​、 ​Take​ ​、 ​TakeWhile​ ​、 ​Skip​ ​、 ​SkipWhile​ ​、 ​Distinct​ ​

9.1.1.2 映射运算符

​IEnumerable<TSource>​ → IEnumerable<TResult>​

此类运算符将数据 映射为另一种形式 :

​Select​、SelectMany​

9.1.1.3 连接运算符

​IEnumerable<TOuter>​,IEnumerable<TInner>​ → IEnumerable<TResult>​

此类运算符用于连接元素:

​ ​Join​ ​、 ​GroupJoin​ ​、 ​Zip​ ​

9.1.1.4 排序运算符

​IEnumerable<TSource>​ → IOrderedEnumerable<TSource>​

此类运算符将返回已排序的序列:

​ ​OrderBy​ ​、 ​ThenBy​ ​、 ​Reverse​ ​

9.1.1.5 分组运算符

​IEnumerable<TSource>​ → IEnumerable<IGrouping<TKey, TElement>>​

此类运算符用于对序列 分组 :

​ ​GroupBy​ ​

9.1.1.6 集合运算符

​IEnumerable<TSource>​,IEnumerable<TSource>​ → IEnumerable<TSource>​

此类运算符用于 合并 序列:

​ ​Concat​ ​、 ​Union​ ​、 ​Intersect​ ​、 ​Except​ ​

9.1.1.7 转换方法:导入

​IEnumerable​ → IEnumerable<TResult>​

此类运算符类似 Select,却仅进行转换:

​OfType​、Cast​

9.1.1.8 转换方法:导出

​IEnumerable<TSource>​ → 数组、列表、字典、lookup 或 IEnumerable<T>​

此类运算符用于将 IEnumerable​ 转化为具体的 集合 类型:

​ToArray​、ToList​、ToDictionary​、ToLookUp​、AsEnumerable​、AsQueryable​

9.1.2 序列 → 元素或标量值(IEnumerable​ → value)

9.1.2.1 元素运算符

​IEnumerable<TSource>​ → TSource​

此类运算符用于 取出一个特定的元素 :

​First​、FirstOrDefault​、Last​、LastOrDefault​、Single​、SingleOrDefault​、ElementAt​、ElementAtOrDefault​、DefaultIfEmpty​

9.1.2.2 聚合方法

​IEnumerable<TSource>​ → scalar

此类运算符对元素进行 计算 ,并返回 一个标量 值:

​ ​Aggregate​ ​、 ​Average​ ​、 ​Count​ ​、 ​LongCount​ ​、 ​Sum​ ​、 ​Max​ ​、 ​Min​ ​

9.1.2.3 量词运算符

​IEnumerable<TSource>​ → bool

此类运算符返回 true​ 或 false​:

​ ​All​ ​、 ​Any​ ​、 ​Contains​ ​、 ​SequenceEqual​ ​

9.1.3 void → 序列(void → IEnumerable​)

9.1.3.1 生成集合的方法

void → IEnumerable<TResult>​

此类运算符可以从零开始输出一个 IEnumerable​:

​ ​Empty​ ​、 ​Range​ ​、 ​Repeat​ ​

9.2 筛选

方法 描述 等价 SQL
​Where​ 返回满足给定条件的元素集合
WHERE
​Take​ 返回前 count 个元素,丢弃剩余的元素 WHERE ROW_NUMBER()...
或
TOP n subqery
​Skip​ 跳过前 count 个元素,返回剩余的元素 WHERE ROW_NUMBER()...
或
NOT IN (SELECT TOP n ...)
​TakeWhile​ 返回输入序列中的元素,直到 predicate 判断为 false 抛出异常
​SkipWhile​ 持续忽略输入序列中的元素,直至 predicate 为 false 。而后返回剩余元素 抛出异常
​Distinct​ 返回一个 没有重复元素的 集合 SELECT DISTINCT ...

9.2.1 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;
}

Where​ 支持两种 ​Func(L2S 和 EF 只支持第一种)​: ​Func<TSource>​ ​ 和 ​Func<TSource, int>​ ​,其 int​ 参数标注当前 TSource​ 元素是第几个。例如:

names.Where ((n, i) => i % 2 == 0).Dump ("Skipping every second element");

9.2.1.5 #suspend#​

后面讲解 L2S 和 EF,暂时略过。

9.2.2 Take​ 和 Skip​ 运算符

​Take ​将返回序列中的前 n 个元素,并放弃其余元素;Skip ​则是放弃前 n 个元素,并返回其余元素。

9.2.3 TakeWhile​ 和 SkipWhile​

假设有如下数组:

int[] numbers = { 3, 5, 2, 234, 4, 1 };

​ ​TakeWhile​ ​ 输出每一个元素,直至给定的断言为 false​,并忽略剩余元素:

// 输出 3、5、2
numbers.TakeWhile (n => n < 100).Dump ("TakeWhile");

​ ​SkipWhile​ ​ 忽略每一个元素,直至给定的断言为 false​,并返回剩余的元素:

// 输出 234、4、1
numbers.SkipWhile (n => n < 100).Dump ("SkipWhile");

9.2.4 Distinct​

​Distinct​ 返回去除 重复元素 后的输入序列,并可以接受(可选的)自定义相等比较器。下面的例子返回去除重复字母的字符串:

// 输出 Helowrd
"HelloWorld".Distinct()

9.3 映射

方法 描述 等价 SQL
​Select​ 将输入中的每一个元素按照给定 Lambda 表达式进行转换 SELECT
​SelectMany​ 将输入的每一个元素按照 Lambda 表达式进行转换,并将嵌套的集合 展平 后连接在一起 INNER JOIN, LEFT OUTER JOIN, CROSS JOIN

9.3.1 Select​

内部实现如下:

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

​Select​ 支持两种 Func(L2S 和 EF 仅支持一种): ​Func<TSource, TResult>​ ​ 和 ​Func<TSource, TResult, int>​ ​,其 int​ 参数标注当前 TSource​ 元素是第几个。例如:

names.Select ((s,i) => i + "=" + s).Dump();
关联子查询

​Select​ 常常用到关联子查询,当子查询引用 外部查询的 对象时,该子查询就是关联的。例如,如下代码子查询引用了 d:

string sampleDirectory = Environment.GetFolderPath  (Environment.SpecialFolder.MyDocuments);

DirectoryInfo[] dirs = new DirectoryInfo (sampleDirectory).GetDirectories();

var query =
from d in dirs
where (d.Attributes & FileAttributes.System) == 0
select new
{
DirectoryName = d.FullName,
Created = d.CreationTime,
Files = from f in d.GetFiles()
where (f.Attributes & FileAttributes.Hidden) == 0
select new { FileName = f.Name, f.Length, }
};

Tips

老实说关联子查询和 SelectMany​ 有点相似。

9.3.1.6 #suspend#​

后面讲解 L2S 和 EF,暂时略过。

9.3.2 SelectMany​

内部实现如下:

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

​SelectMany​ 可用于展开子序列,将嵌套的集合平面化;或将两个集合合并。例如:

var fullNames = new[] { "Anne Williams", "John Fred Smith", "Sue Green" }.AsQueryable();

// 将字符串拆分,输出 Anne Williams John Fred Smith Sue Green
IEnumerable<string> query2 = fullNames.SelectMany (name => name.Split());
var numbers = new[] { 1, 2, 3 }.AsQueryable();
var letters = new[] { "a", "b" }.AsQueryable(); // 将两部分内容合并,输出 1a 1b 2a 2b 3a 3b
IEnumerable<string> query =
from n in numbers
from l in letters
select n.ToString() + l;
​SelectMany​ 的查询语法

查询语法中不存在 SelectMany​,需要使用 两层 ​from​ ​:

IEnumerable<string> query3 =
from fullName in fullNames
from name in fullName.Split() // 编译器自动转化为 SelectMany
select name;

查询语法相较流式语法,之前定义的变量仍能使用。以如下代码为例,该语句将转化为如下流式语法:

IEnumerable<string> query =
from fullName in fullNames
from name in fullName.Split()
select name + " came from " + fullName;
System.String[]
.SelectMany (
fullName => fullName.Split (new Char[0]),
(fullName, name) => new { fullName, name }
)
.Select (temp0 => ((temp0.name + " came from ") + temp0.fullName))

从这一点可以看到查询语法的简洁之处。

9.3.2.7 #suspend#​

后面讲解 L2S 和 EF,暂时略过。

9.4 连接

方法 描述 等价的 SQL
​Join​ 使用查找规则对两个集合的元素进行匹配,返回平面的结果集 INNER JOIN
​GroupJoin​ 同上,但返回层次化的结果集 INNER JOIN, LEFT OUTER JOIN
​Zip​ 同时依次枚举两个序列(像拉链一样),对每一对元素执行指定的函数 抛出异常

9.4.1 Join​ 和 GroupJoin​

​Join​ 和 GroupJoin​ 将两个输入序列连接为一个输出序列。Join​ 生成平面的输出,GroupJoin​ 生成层次化的输出。相较 Select(Many)​,(Group)Join​ 对本地 IEnumerable​ 效率更高。差别如下:

C7.0 核心技术指南 第7版.pdf - p466 - C7.0 核心技术指南 第 7 版-P466-20240412171315-wt32ctd

​​

9.4.1.5 Join​

语法如下:

\(join \quad 内部变量 \quad in \quad 内部序列 \quad on \quad 外部键表达式 \quad equals \quad 内部键表达式\)

​Join​ 在查询时会将内部集合预加载至表中,因此效率更高。如下代码(Join​ 和 SelectMany​)结果一致,Join​ 的效率更高:

var slowQuery =
from c in customers
from p in purchases
where c.ID == p.CustomerID
select c.Name + " bought a " + p.Description;
var fastQuery =
from c in customers
join p in purchases on c.ID equals p.CustomerID
select c.Name + " bought a " + p.Description;

​Join​ 可以多次使用:

from c in Customers
join p in Purchases on c.ID equals p.CustomerID // first join
join pi in PurchaseItems on p.ID equals pi.PurchaseID // second join
select new { c.Name, p.Description, pi.Detail }

Eureka

​Join​ 的本质其实是“筛选”,将两个序列键相同的部分筛选出来。

9.4.1.6 基于多个键的 Join​

我们可以通过匿名类型在一个查询中基于多个键进行连接查询:

from s in stringProps
join b in builderProps on new { s.Name, s.PropertyType }
equals new { b.Name, b.PropertyType }

需要注意,两个匿名类型在 结构 上必须一致。

9.4.1.7 Join​ 在流式语法中的使用

流式语法和对应的查询语法如下

var fluentSyntax =
Customers.Join ( // outer collection
Purchases, // inner collection
c => c.ID, // outer key selector
p => p.CustomerID, // inner key selector
(c, p) => new // result selector
{
c.Name, p.Description, p.Price
}
);
var querySyntax =
from c in Customers
join p in Purchases on c.ID equals p.CustomerID
select new
{
c.Name, p.Description, p.Price
};

可以看到,查询语法更为简洁。

使用流式语法时,若希望在 Join​ 之后添加其他子句(如 OrderBy​),则必须引入一个 匿名类型 。例如:

var querySyntax =
from c in Customers
join p in Purchases on c.ID equals p.CustomerID
orderby p.Price
select c.Name + " bought a " + p.Description + " for $" + p.Price;
var fluentSyntax =
Customers.Join ( // outer collection
Purchases, // inner collection
c => c.ID, // outer key selector
p => p.CustomerID, // inner key selector
(c, p) => new { c, p } // result selector
)
.OrderBy (x => x.p.Price)
.Select (x => x.c.Name + " bought a " + x.p.Description + " for $" + x.p.Price);

9.4.1.8 GroupJoin​ #delay#​ 看不懂,略过

在查询语法中,GroupJoin​ 和 Join​ 一致,但需要和 into​ 关键字配合使用:

from c in Customers.AsEnumerable()
join p in Purchases on c.ID equals p.CustomerID
into custPurchases
select custPurchases

C7.0 核心技术指南 第7版.pdf - p469 - C7.0 核心技术指南 第 7 版-P469-20240413155101-k4ab81b

​​

9.4.2 Zip​ 运算符

​Zip​ 运算符就像拉链,同时枚举两个集合中的元素,返回经过处理的元素对。用法如下:

int[] numbers = { 3, 5, 7 };
string[] words = { "three", "five", "seven", "ignored" };
// 产生序列:3=three 5=five 7=seven
IEnumerable<string> zip = numbers.Zip (words, (n, w) => n + "=" + w);

序列中不匹配的元素会 忽略 。

Error

L2S 和 EF 不支持 Zip​

Info

另见Enumerate & Zip

9.5 排序

方法 描述 等价 SQL
​OrderBy​ ThenBy​ 将序列按升序排序 ORDER BY
​OrderByDescending​
ThenByDescending​
将序列按降序排序 ORDER BY ... DESC
​Reverse​ 将一个序列进行反转 抛出异常

9.5.1 OrderBy​、OrderByDescending​、ThenBy​ 和 ThenByDescending​

9.5.1.1 OrderBy​

​OrderBy​ 根据委托的返回值确定元素先后,以如下代码为例:

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };
names.OrderBy (s => s.Length).Dump ("Ordered by length");
// 排序结果
Tom Jay Dick Mary Harry

9.5.1.2 ThenBy​

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };
names.OrderBy (s => s.Length).Dump ("Ordered by length");

可以注意到,Tom 和 Jay,Dick 和 Mary 字符长度相同,顺序不确定。此时我们可以使用 ThenBy​ 进行二次排序:

names.OrderBy (s => s.Length).ThenBy (s => s)
.Dump ("By length, then alphabetically");

对于 ThenBy​ 二次排序后仍未确定顺序的元素,可以用 ThenBy​ 再次排序。ThenBy​ 可以套用多次,如下两段代码等价:

names.OrderBy (s => s.Length).ThenBy (s => s[1]).ThenBy (s => s[0])
.Dump ("By length, then second character, then first character");
(
from s in names
orderby s.Length, s[1], s[0]
select s
)
.Dump ("Same query in query syntax");

9.5.1.3 比较和核对

如果我们想自定义排序方式,可以传入 IComparer 对象至运算符中:

names.OrderBy (n => n, StringComparer.CurrentCultureIgnoreCase)
.Dump ("Case insensitive ordering");

注意:查询语法、L2S 和 EF 不支持自定义比较器。

​IOrderedEnumerable​ ​ 和 ​IOrderedQueryable​ ​

上述运算符返回类型均为 ​IOrderedEnumerable<TSource>​ ​,其内部保留了排序信息,以支持 Thenby​ 二次排序。

9.6 分组

9.6.1 GroupBy​

查询语法如下:

\(group \quad 元素表达式 \quad by \quad 键表达式\)

​GroupBy​ 可以对一个平面输入序列进行 分组 ,它在内部创建了一个临时的 列表字典 。它的返回类型是 ​IEnumerable<IGrouping<TKey, TElement>>​ ​,用法如下:

string[] files = Directory.GetFiles (Path.GetTempPath()).Take (100).ToArray();

files.GroupBy (file => Path.GetExtension (file))
.Dump ("Your temporary files, grouped by extension.");

9.6.1.1 keySelector​ 和 elementSelector​

public static IEnumerable<IGrouping<TKey, TElement>> GroupBy<this TSource, TKey, TElement>
(IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector)

对于组内的元素,我们可以通过 elementSelector​ 委托修改后再分组。如下代码将群组中的元素转为了大写:

files.GroupBy (file => Path.GetExtension (file), file => file.ToUpper())
.Dump ("In Fluent Syntax");

Question

​Join​ 的 Selector 和 GroupBy​ 的 Selector 有什么区别?

9.6.1.2 GroupBy​ 的查询表达式

如下两段代码等价:

(
from file in files
group file.ToUpper() by Path.GetExtension (file)
)
.Dump ("In query syntax");
files.GroupBy (file => Path.GetExtension (file), file => file.ToUpper())
.Dump ("In Fluent Syntax");

正如在 8.3 查询表达式提到:

查询表达式一般以 ​from​ ​子句开始,最后以 ​select​ ​或者 ​group​ ​子句结束。

若想在 GroupBy​ 后继续查询,需要使用 ​into​ ​ 关键字:

from file in files
group file.ToUpper() by Path.GetExtension (file) into grouping
orderby grouping.Key
select grouping

9.6.1.3 根据多个键进行分组

在9.4.1.6 基于多个键的 Join中我们使用了匿名类型辅助连接,GroupBy​ 中也可以通过匿名类型辅助分组:

from n in new[] { "Tom", "Dick", "Harry", "Mary", "Jay" }
group n by new
{
FirstLetter = n[0],
Length = n.Length
}

Eureka

在查询表达式中,group ... by ...​ 允许我们定义一个键选择器,这个键选择器可以是一个属性、一个方法调用或任何返回一个值的表达式,类似的有:

var groupedByAgeRange = from person in people
group person by (person.Age < 30 ? "Below 30" :
person.Age <= 40 ? "Between 30 and 40" : "Above 40") into ageGroup
select new { AgeRange = ageGroup.Key, Members = ageGroup };

上述代码 group​ 代码将比较返回的字符串是否相同

9.6.1.4 自定义相等比较器

​GroupBy​ 的本地查询可以传入相等比较器,方法签名如下:

public static IEnumerable<IGrouping<TKey, TSource>> GroupBy<TSource, TKey>
(IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer)

这种方式并不常用,一般会更改 keySelector​ 委托(表达式):

group name by name.ToUpper()

9.7 集合运算符

方法 描述 等价 SQL 语句
​Concat​ 拼接两个序列 UNION ALL
​Union​ 并 集 UNION
​Intersect​ 交 集 WHERE ... IN
​Except​ 差 集 EXCEPT
或
WHERE ... NOT IN

9.7.1 Concat​ 和 Union​

​ ​Concat​ ​ 将两个集合拼接; ​Union​ ​ 与 ​Concat​ ​ 类似,但是会去掉重复元素。如下代码分别输出 ​{ 1, 2, 3, 3, 4, 4, 5 }​ ​ 和 ​{ 1, 2, 3, 4, 5 }​ ​:

int[] seq1 = { 1, 2, 3 }, seq2 = { 3, 4, 4, 5 };

seq1.Concat (seq2).Dump ("Concat");
seq1.Union (seq2).Dump ("Union");

两个有共同基类的 IEnumerable​ 实例,可以指定泛型参数类型后进行拼接:

MethodInfo[] methods = typeof (string).GetMethods();
PropertyInfo[] props = typeof (string).GetProperties();
IEnumerable<MemberInfo> both = methods.Concat<MemberInfo> (props);

9.7.2 Intersect​ 和 Except​

​ ​Intersect​ ​ 返回两个集合的交集, ​Except​ ​ 返回相较第 二 个集合的差集:

int[] seq1 = { 1, 2, 3 }, seq2 = { 3, 4, 5 };

seq1.Intersect (seq2).Dump ("Intersect");            // { 3 }
seq1.Except (seq2).Dump ("seq1.Except (seq2)"); // { 1, 2 }
seq2.Except (seq1).Dump ("seq2.Except (seq1)"); // { 4, 5 }

​Except​ 内部将第一个集合的元素加载至字典,并移除字典中在第二个集合中出现的元素。

9.8 转换方法

方法 描述
​ ​OfType​ ​ 将 IEnumerable​ ​转换为 IEnumerable<T>​,如果转换过程中有错误类型的元素,则丢弃该元素
​ ​Cast​ ​ 将 IEnumerable​ ​转换为 IEnumerable<T>​,如果转换过程中有错误类型的元素,则抛出异常
​ToArray​ 将 IEnumerable<T>​ ​转换为 T[]​
​ToList​ 将 IEnumerable<T>​ ​转换为 List<T>​
​ToDictionary​ 将 IEnumerable<T>​ ​转换为 Dictionary<TKey,TValue>​
​ ​ToLookup​ ​ 将 IEnumerable<T>​ ​转换为 ILookup<TKey,TElement>​
​AsEnumerable​ 将集合类型转换为 IEnumerable<T>​
​AsQueryable​ 将集合类型转换或转化为 IQueryable<T>​

9.8.1 OfType​ 和 Cast​

​OfType ​和 Cast ​接受非泛型的 IEnumerable ​集合并生成泛型的 IEnumerable<T> ​序列,进而执行后续的子查询。

ArrayList classicList = new ArrayList();
classicList.AddRange ( new int[] { 3, 4, 5 } ); IEnumerable<int> sequence1 = classicList.Cast<int>();

对于不兼容的类型, ​OfType​ ​会忽略,继续处理后续元素; ​Cast​ ​ 会抛出异常:

// 数据
ArrayList classicList = new ArrayList();
classicList.AddRange ( new int[] { 3, 4, 5 } ); classicList.Add (DateTime.Now);
IEnumerable<int>
ofTypeSequence = classicList.OfType<int>(),
castSequence = classicList.Cast<int>(); ofTypeSequence.Dump ("不兼容的“DateTime”将不会输出");
try {
castSequence.Dump();
}
catch (InvalidCastException ex) {
ex.Message.Dump ("因包含不兼容的元素,转化失败");
}

元素兼容规则

​OfType​ 的转换遵循 C# 的 is​ 运算符规则,仅允许 引用 转换和 拆箱 转换。

​OfType​ 的内部实现如下:

public static IEnumerable<TSource> OfType <TSource> (IEnumerable source)
{
foreach (object element in source)
if (element is TSource)
yield return (TSource)element;
}

​Cast​ 的内部实现相似,但忽略了兼容性检测:

public static IEnumerable<TSource> Cast <TSource> (IEnumerable source)
{
foreach (object element in source)
yield return (Tsource)element;
}

Eureka

从上述代码可知,OfType​ 和 Cast​ 是 IEnumerable​ 的扩展方法(而非 IEnumerable<T>​),值类型参数也会被装箱,因此它们无法执行 自定义 转换和 数值 转换。

此外,装箱和拆箱、3.9.11 类型参数的转换也提到,泛型变量只支持引用转换,不支持装箱转换。

以代码为例:

int[] integers = {1,2,3};
IEnumerable<long> test1 = integers.OfType<long>();
IEnumerable<long> test2 = integers.Cast<long>();

​test2​ 将抛出异常,原因如下:

C7.0 核心技术指南 第7版.pdf - p483 - C7.0 核心技术指南 第 7 版-P483-20240413210650-whsmvj7

​​

​Cast​ 查询语法

​Cast​ 的查询语法通过在 from​ 语句中定义类型实现,用法如下:

object[] untyped = { 1, 2, 3 };

var query1 =
from i in untyped.Cast<int>() // Without syntactic shortcut
select i * 10;
object[] untyped = { 1, 2, 3 };

var query2 =
from int i in untyped // Notice we've slipped in "int"
select i * 10;

9.8.2 ToArray​、ToList​、ToDictionary​ 和 ToLookup​

​ToDictionary​ 和 ToLookup​ 不同点在于, ​ToDictionary​ ​ 不允许元素的 Key 重复,而 ​ToLookup​ ​ 可以:

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

names.ToLookup(n => n.Length).Dump();       // 正常
names.ToDictionary(n => n.Length).Dump(); // 运行时报错

​​

Eureka

​ToLookup​ 和 ​GroupBy​ ​ 相同,唯一的区别是 ​GroupBy​ ​ 可以延迟执行。

9.9 元素运算符

方法 描述 等价 SQL
​ ​First​ ​
​FirstOrDefault​ ​
返回序列中的第一个元素,可添加可选的断言。 SELECT TOP 1 ... ORDER BY ...
​ ​Last​ ​
​LastOrDefault​ ​
返回序列中的最后一个元素,可添加可选的断言。 SELECT TOP 1 ... ORDER BY ... DESC
​ ​Single​ ​
​SingleOrDefault​ ​
和 First​/FirstOrDefault ​几乎等价,但是在多于一个匹配时抛出异常
​ ​ElementAt​ ​
​ElementAtOrDefault​ ​
返回特定位置的元素 抛出异常
​ ​DefaultIfEmpty​ ​ 当序列没有任何元素时,返回一个单元素序列,且元素值为 default(TSource)​ OUTER JOIN

​default(TSource)​ 对于引用类型元素为 null,对于 bool​ 类型为 false​,而对于数值类型为零。

9.9.1 First​、Last​ 和 Single​

​ ​Single​ ​ 运算符要求集合中有且仅有一个匹配元素; ​SingleOrDefault​ ​ 则允许集合中有一个或者零个匹配元素,如下代码运行结果分别是:

int[] numbers = { 1, 2, 3, 4, 5 };

numbers.Single(n => n % 3 == 0);
numbers.Single(n => n % 2 == 0); numbers.Single(n => n > 10);
numbers.SingleOrDefault(n => n > 10);
numbers.SingleOrDefault(n => n % 2 == 0);
  1. 3
  2. 错误
  3. 错误
  4. 0
  5. 错误

在 L2S 和 EF 中,Single​ 运算符通常用在使用主键从数据库中检索一行数据的场景:

Customer cust = Customers.Single (c => c.ID == 3);

9.9.2 ElementAt​ 运算符

​Enumerable.ElementAt​ 在输入序列恰好实现了 ​IList<T>​ ​ 的情况下会直接调用 ​IList<T>​ ​的索引器。否则它将枚举 n 次,并返回下一个元素。

L2S 和 EF 不支持 ElementAt​ 运算符。

9.9.3 DefaultIfEmpty​

​DefaultIfEmpty​ 在输入序列为空序列(而非 null)时返回只包含一个 default(TSource)​ 元素的序列,否则返回原始的输入序列。

它主要用来实现展平结果集的外连接。请参见 9.3.2.8 节和 9.4.1.9 节。

9.10 聚合方法

方法 描述 等价 SQL
​ ​Count​ ​
​LongCount​ ​
返回输入序列中元素的个数,可以添加可选的断言 ​COUNT(...)​
​ ​Min​ ​
​Max​ ​
返回序列中的最小和最大的元素 ​MIN(...)​
MAX(...)​
​ ​Sum​ ​
​Average​ ​
计算序列中元素的和或平均数 ​SUM(...)​
AVG(...)​
​Aggregate​ 执行自定义聚合方法 异常抛出

9.10.2 Min​ 和 Max​

输入的序列若未实现 ​IComparable<T>​ ​ 接口,则需要提供 selector​ 表达式。

9.10.3 Sum​ 和 Average​

​Sum​ 和 Average​ 对类型的要求比较严格,序列元素必须是数值类型。对于 ​Average​ ​,其返回值只能是 decimal​、float​ 或 double​。可以参考下表:

C7.0 核心技术指南 第7版.pdf - p487 - C7.0 核心技术指南 第 7 版-P487-20240414144821-5zw4gan

​​

​Average​ 会将输入值隐式向上转换以避免精度损失。

9.10.4 Aggregate​

​Aggregate​ 用于实现自定义聚合算法,如下例子实现了 Sum​ 的功能:

int[] numbers = { 1, 2, 3 };
numbers.Aggregate (0, (seed, n) => seed + n).Dump();

​Aggregate​ 有三个参数:

  1. 种子,指定积累结果的 初始 值;
  2. ​Func<>​,用于自定义 聚合算法 ;
  3. 映射最终值,可选参数。

该运算符在 L2S 和 EF 中无法使用。

C7.0 核心技术指南 第7版.pdf - p488 - C7.0 核心技术指南 第 7 版-P488-20240414154118-zhicuhk

​​

9.10.4.1 无种子的聚合操作

在使用 Aggregate​ 运算符时可以省略种子。编译器会自动将集合的第一个元素隐式地作为种子。例如,如下代码运行额结果是 0 6 :

int[] numbers = { 1, 2, 3 };
numbers.Aggregate(0, (prod, n) => prod * n).Dump();
numbers.Aggregate((prod, n) => prod * n).Dump();

9.10.4.2 无种子 Aggregate​ 的陷阱

无种子聚合方法要求传入的委托满足交换性和结合性,否则计算结果将出现不一致。例如如下代码计算结果分别为 27 29 :

// (total, n) => total + n * n
int[] numbers={2, 3, 4};
numbers.Aggregate ((total, n) => total + n * n).Dump();
numbers.Aggregate (0, (total, n) => total + n * n).Dump();

此外,LINQ to Object 和 PLINQ 的计算逻辑也有不同。对于 f(total, n) => total + n * n​:

// LINQ to Object 的计算方式
f(f(f(0, 2), 3), 4)
// PLINQ 的计算方式
f(f(0, 2), f(3, 4))

C7.0 核心技术指南 第7版.pdf - p490 - C7.0 核心技术指南 第 7 版-P490-20240414155721-3kv2xn6

​​

9.11 量词运算符

方法 描述 等价 SQL
​ ​Contains​ ​ 如果给定序列中包含指定元素则返回 true WHERE ... IN (...)
​ ​Any​ ​ 如果序列中有任何元素满足给定的断言则返回 true WHERE ... IN (...)
​ ​All​ ​ 如果序列中所有元素都满足给定断言则返回 true WHERE (...)
​ ​SequenceEqual​ ​ 如果第二序列和输入序列包含的元素相同则返回 true

9.11.1 Contains​ 和 Any​

​ ​Contains​ ​ 方法接受一个 TSource​ 类型的参数;而 ​Any​ ​ 则接受一个可选的断言。​Any​ 包含了 Contains​ 的所有功能。

Tips

​Any​ 常用在数据库查询的子查询(即嵌套查询)中,例如:

from c in dataContext.Customers
where c.Purchases.Any (p => p.Price > 10_000)
select c

9.11.2 All​ 和 SequenceEqual​

​All​ 运算符,当序列中的所有元素都满足断言时,返回 true。

Customers.Where (c => c.Purchases.All (p => p.Price < 100))

​SequenceEqual​ 运算符比较两个序列。当两个序列都含有相同的元素,且顺序也相同,SequenceEqual​ 返回 true。如下代码运行结果为 true :

var query = "Hello".Distinct();

query.SequenceEqual("Helo").Dump();

​SequenceEqual​ 支持可选的相等比较器,默认的比较器为 ​EqualityComparer<T>.Default​ ​。

9.12 生成集合的方法

方法 描述
​ ​Empty​ ​ 创建一个空序列
​ ​Repeat​ ​ 创建一个含有重复元素的序列
​ ​Range​ ​ 创建一个整数序列

​Empty​、Repeat​ 和 Range​ 都是静态的非扩展方法,它们只能创建简单的本地序列。

9.12.1 Empty​

​Empty​ 仅接收一个类型参数,该运算符将创建一个 空序列 。它可以和 ??​ 运算符配合使用,防止 null 元素。以如下代码为例,第 一 组代码将抛出异常:

// 数据
int[][] numbers =
{
new int[] { 1, 2, 3 },
new int[] { 4, 5, 6 },
null
};
IEnumerable<int> flat = numbers.
SelectMany (innerArray => innerArray).Dump();
IEnumerable<int> flat = numbers
.SelectMany (innerArray => innerArray ?? Enumerable.Empty<int>()).Dump();

Question

试比较 Empty​ 和 DefaultIfEmpty​。9.9.3 DefaultIfEmpty

第9章 LINQ 运算符的更多相关文章

  1. C#图解教程 第十九章 LINQ

    LINQ 什么是LINQLINQ提供程序 匿名类型 方法语法和查询语法查询变量查询表达式的结构 from子句join子句什么是联结查询主体中的from-let-where片段 from子句let子句w ...

  2. C Primer Plus_第5章_运算符、表达式和语句_编程练习

    Practice 1. 输入分钟输出对应的小时和分钟. #include #define MIN_PER_H 60 int main(void) { int mins, hours, minutes; ...

  3. python 教程 第三章、 运算符与表达式

    第三章. 运算符与表达式 1)    运算符 + 加 - 减 * 乘 ** 幂 / 除 // 取整除 % 取模 << 左移 >> 右移 & 按位与 | 按位或 ^ 按位 ...

  4. 第04章_MySQL运算符详解

    第04章_运算符 1. 算术运算符 算术运算符主要用于数学运算,其可以连接运算符前后的两个数值或表达式,对数值或表达式进行加(+).减(-).乘(*).除(/)和取模(%)运算. 1.加法与减法运算符 ...

  5. 第5章 LINQ

    5.4 LINQ查询运算符 using System; using System.Collections.Generic; using System.Linq; using System.Text; ...

  6. C#高级编程9 第11章 Linq

    Linq 1.Linq概述 列表和实体 准备数据: public class Championship { public int Year { get; set; } public string Fi ...

  7. C#高级编程 (第六版) 学习 第六章:运算符和类型强制转换

    第六章 运算符和类型强制转换 1,运算符 类别 运算符 算术运算符 + - * / % 逻辑运算符 & | ^ ~ && || ! 字符串连接运算符 + 增量和减量运算符 ++ ...

  8. C#高级编程(第9版) 第11章 LINQ 笔记

    概述语言集成查询(Language Integrated Query, LINQ)在C#编程语言中集成了查询语法,可以用相同的语法访问不同的数据源.LINQ提供了不同数据源的抽象层,所以可以使用相同的 ...

  9. 《精通C#》第十二章 Linq

    Linq是在.Net3.5之后首次引入的,这种查询语言简单易学,可用范围非常广泛在学着之前,一直用在数据库操作之上,但是在学习这节课之后才发现,凡是实现泛型的接口类型都可以使用linq,简单来说就是实 ...

  10. Swift中文教程 第2章 基本运算符 (20140604更新)

    今天我在下午又进行了新的翻译,之前我翻译的大家有没有看啊,感觉如何,我昨天下午不小心点了什么原先的文章都没了,大家的评论也没了,现在我也找不回来了,希望大家能继续给我新的评论和支持点个推荐,毕竟现在学 ...

随机推荐

  1. php 超过64位进制和10进制的转换

    有时候想把一个很大的数尽量用更少的空间存储起来,那么就可以采用很大的进制来存储它,比如说,一个大于等于10小于等于16数字使用10进制就需要两位,使用16进制就只需要1位,那就等于帮程序省了一位的空间 ...

  2. Solr的学习使用之(七)Solr高级查询facet、facet.pivot简介 - OnTheRoad_Lee

    http://martin3000.iteye.com/blog/1330106 1 .什么是Faceted Search Facet['fæsɪt]很难翻译,只能靠例子来理解了.Solr作者Yoni ...

  3. Rework:每个程序员都应该读的一本书

    来源: 萌萌的博客 每一个程序员都有改变世界的梦想,他们不甘平凡,他们想要与众不同,他们想要创立世界上最酷的公司,那具体该如何做呢?风靡全球的<Rework>将告诉你答案. 37signa ...

  4. Java里快如闪电的线程间通讯

    这个故事源自一个很简单的想法:创建一个对开发人员友好的.简单轻量的线程间通讯框架,完全不用锁.同步器.信号量.等待和通知,在Java里开发一个轻量.无锁的线程内通讯框架:并且也没有队列.消息.事件或任 ...

  5. Converter Tutorial

    Setting up a simple example This is the most basic converter... let's start with a simple Person: pa ...

  6. git 推送代码到多个 远端仓库

    业务场景 在开发代码时,有时希望将代码推送到两个远端仓库. 实现方法 git remote add origin giturl1 git remote add backup giturl2 git p ...

  7. fastapi 实现HTTP访问

    1.概述 在使用python 时,我如何发布一个接口给外部访问, python 有 FASTAPI 和 uvicorn 实现,fastapi 是定义 api接口,uvicorn 运行服务器. 2.安装 ...

  8. [NET,C# ] Nuget包发布流程

    1.新建一个.NET Core类库 2.新增一个方法,并编译项目 3.下载Nuget.exe,与刚才新建的类库放在同一目录下 下载地址:https://www.nuget.org/downloads ...

  9. 奇奇怪怪的编程语言:Malbolge

    Malbolge 除了我们日常使用的Python.Java.C等主流编程语言外,还存在这么一类极为晦涩难懂的编程语言,被称为深奥的编程语言(Esoteric programming language, ...

  10. vue3 学习笔记(不断更新中...)(2024.11.13)

    组合式API setup() 11 响应式API ref ref 用于创建响应式数据(通常用来定义 基本类型数据) 在JavaScript代码中,需要使用 .value 来操作数据 let count ...