第9章 LINQ 运算符
本章所有例子所使用的 names 数组都是一致的:
string[] names = {"Tom", "Dick", "Harry", "Marry", "Jay" };
9.1 概述
标准查询运算符分三类:
- 输入是序列(
IEnumerable
),输出是序列(IEnumerable
)(IEnumerable
→IEnumerable
) - 输入是序列(
IEnumerable
),输出是单个元素或标量值(IEnumerable
→ value) - 没有输入,输出是序列(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);
- 3
- 错误
- 错误
- 0
- 错误
在 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
有三个参数:
- 种子,指定积累结果的 初始 值;
-
Func<>
,用于自定义 聚合算法 ; - 映射最终值,可选参数。
该运算符在 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();
标签:Dump,元素,IEnumerable,LINQ,运算符,numbers,序列 From: https://www.cnblogs.com/hihaojie/p/18639816/chapter-9-linq-operator-1s6ltfQuestion
试比较
Empty
和DefaultIfEmpty
。9.9.3 DefaultIfEmpty