第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
OrderByThenBy 将序列按升序排序 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. 关于switch的优化:转移表(完结)

    用转移表来代替功能实现中的 switch 语句 1. switch 语句应用场景 在许多场景中会用到条件判断,此时简单的条件判断一般通过 if/else 语句实现:如果涉及三条以上的功能分支一般会通过 ...

  2. C#/.NET/.NET Core技术前沿周刊 | 第 14 期(2024年11.18-11.24)

    前言 C#/.NET/.NET Core技术前沿周刊,你的每周技术指南针!记录.追踪C#/.NET/.NET Core领域.生态的每周最新.最实用.最有价值的技术文章.社区动态.优质项目和学习资源等. ...

  3. java——棋牌类游戏五子棋(webwzq1.0)之一(基础)

    这是本人最近一段时间写的斗地主的java代码,大体框架都实现了,一些细节还需要后续优化. package basegame; import java.awt.Button; import java.a ...

  4. Git之清除历史记录操作

    近期公司需要将之前代码仓库中的提交记录都清理,所以操作一下,记录一下步骤: 安全考虑: 有时候在提交代码时,不小心提交了敏感数据,如账号密码什么的,这样在历史记录中就可以查看到,这样很不安全,所以就需 ...

  5. tmux之常见问题

    1. 使用tmux ls的时候显示错误 failed to connect to server: Connection refused 解决: 查看进程是否存在 ps -aux|grep tmux 发 ...

  6. Phpstorm之快捷键

    常用快捷键 1.ctrl+alt+s 快速打开setting系统设置 2.CTRL+/ 单行注释/取消注释 CTRL+SHIFT+/ 块状注释/取消块状注释 3.方法体上面打入'/**' 再按回车键 ...

  7. # 为什么要使用 MediatR 的 3 个理由和 1 个不使用它的原因

    为什么要使用 MediatR 的 3 个理由和 1 个不使用它的原因 https://codeopinion.com/why-use-mediatr-3-reasons-why-and-1-reaso ...

  8. go语言签发和验证license

    https://www.cnblogs.com/guangdelw/p/18328342 生成非对称密钥 package main import ( "crypto/rand" & ...

  9. gitlab runner install

    gitlab-runner install -d /home/gitlab-runner/ --syslog --user gitlab-runner

  10. Linux readonly

    1. 概念readonly在shell脚本中,用来标记变量是只读的,后续执行过程中就不能对其值进行改变,这个用来定义一些常量类的变量. 如果对其进行重新赋值,会提示错误 [root@localhost ...