C# 试题 28:LINQ 分组与连接
0541 关于 Enumerable.GroupBy 的执行模型,哪项正确?
难度: 进阶
- A. GroupBy 调用时立即返回分组结果
- B. GroupBy 是延迟的:调用只构建查询,枚举时才读取整个源并构建分组;因此不能用于无限流
- C. GroupBy 是流式的,边读边输出组
- D. GroupBy 每枚举一个元素就输出一组
查看答案与解析
正确答案B
解析: GroupBy 属于延迟执行,但枚举时它会把源完整读入内部查找结构后才开始产出分组——既不是立即执行,也不是流式。对无限序列调用会一直读下去。
关键点: “延迟”与“流式”是两个维度,GroupBy 延迟但全量物化。
0542 以下代码输出什么?
难度: 实战
var query = new[] { 3, 1, 2, 3, 1 }.GroupBy(x => x);
Console.WriteLine(string.Join("", query.Select(g => g.Key)));
- A.
312,组按键首次出现的顺序输出 - B.
123,组按键排序 - C.
3311 - D.
321,按最后出现顺序
查看答案与解析
正确答案A
解析: 默认实现下分组按键首次出现的顺序产出:3 最先出现,随后 1、2,因此组键序列为 3、1、2。不要假设 GroupBy 按键排序——那需要显式 OrderBy。
关键点: GroupBy 的组序是首次出现序,不是排序序。
0543 以下代码输出什么?
难度: 进阶
var list = new List<int> { 1, 2, 3 };
var query = list.GroupBy(x => x % 2);
list.Add(4);
Console.WriteLine(string.Join(" ", query.Select(g => $"{g.Key}:{string.Join("", g)}")));
- A.
1:13 0:2 - B.
0:24 1:13 - C.
1:13 0:24,枚举时读取最新源,键 0 的组含 2 与新增的 4 - D.
1:1 0:2 1:3 0:4
查看答案与解析
正确答案C
解析: GroupBy 延迟执行,string.Join 枚举时才读取源,此时源为 4:键 1 组 3,键 0 组 4。组序按首次出现(1 先于 0),输出 1:13 0:24。
关键点: 分组结果同样受“枚举时源状态”支配。
0544 以下代码输出什么?
难度: 进阶
var query = new[] { 1, 2, 3, 4 }.GroupBy(x => x % 2);
var evens = query.First(g => g.Key == 0);
Console.WriteLine(evens.Sum());
Console.WriteLine(evens.Sum());
- A. 抛
InvalidOperationException,组只能枚举一次 - B.
6/ 抛异常 - C.
6/0 - D.
6/6,组元素已物化存储,可反复枚举
查看答案与解析
正确答案D
解析: 枚举 GroupBy 查询时,每个组内部的元素已物化到内部存储(不是一次性迭代器),因此同一个组可以多次安全枚举,两次 Sum 都是 2+4=6。这与单次使用的原始迭代器不同。
关键点: GroupBy 的组可重复枚举,别与一次性枚举器混淆。
0545 以下代码输出什么?
难度: 实战
var words = new[] { "a", "A", "b", "B" };
foreach (var g in words.GroupBy(w => w, StringComparer.OrdinalIgnoreCase))
Console.Write(g.Count());
- A.
1111 - B.
22,忽略大小写后分为两组 - C.
4 - D. 抛异常,GroupBy 不支持自定义比较器
查看答案与解析
正确答案B
解析: 传入 StringComparer.OrdinalIgnoreCase 后,“a”与“A”、“b”与“B”分别并入同组,得到两个各含 2 个元素的组,输出 22。默认比较器区分大小写时会分成 4 组。
关键点: 键比较器决定哪些键“相等并归组”。
0546 以下代码输出什么?
难度: 进阶
var query = new[] { "a", null, "b" }.GroupBy(s => s);
foreach (var g in query)
Console.Write($"{g.Key ?? "null"}:{g.Count()} ");
- A.
a:1 null:1 b:1,null 是合法分组键 - B.
a:1 b:1,null 键被丢弃 - C. 抛
ArgumentNullException - D.
a:1 :1 b:1,null 键变成空字符串
查看答案与解析
正确答案A
解析: 引用类型的 null 可以作为分组键,GroupBy 不会抛异常,null 键的组正常存在,Count 为 1。容易误以为“键不能为 null”而漏掉该分支。
关键点: null 键合法,处理分组结果时记得判空。
0547 以下代码输出什么?
难度: 实战
var n = 0;
IEnumerable<int> Source()
{
for (int i = 0; i < 3; i++) { n++; yield return i; }
}
var grouped = Source().GroupBy(x => x % 2);
Console.WriteLine(n);
var lookup = Source().ToLookup(x => x % 2);
Console.WriteLine(n);
- A.
0/0 - B.
3/3 - C.
3/0 - D.
0/3,GroupBy 延迟;ToLookup 立即物化
查看答案与解析
正确答案D
解析: GroupBy 不枚举源,第一次打印 n=0;ToLookup 立即枚举并构建查找表,第二次打印 n=3。两者结果形态相似,但执行时机一个延迟一个立即,是高频混淆点。
关键点: ToLookup 立即执行,GroupBy 延迟执行。
0548 以下代码输出什么?
难度: 实战
var lookup = new[] { 1, 2, 3 }.ToLookup(x => x % 2);
Console.WriteLine(lookup[5].Count());
- A. 抛
KeyNotFoundException - B.
1 - C.
0,ILookup 索引器对缺失键返回空序列 - D.
null
查看答案与解析
正确答案C
解析: ILookup 的索引器对不存在的键返回空序列而不是抛异常,因此 lookup[5] 是零元素序列,Count 为 0。这与字典索引器的契约截然不同。
关键点: Lookup 缺失键返回空序列,不抛异常。
0549 以下代码运行结果是什么?
难度: 实战
var dict = new[] { "a", "b", "a" }.ToDictionary(x => x);
- A. 正常运行,得到两个键
- B. 运行抛
ArgumentException(重复键) - C. 运行抛
InvalidOperationException - D. 编译失败
查看答案与解析
正确答案B
解析: ToDictionary 遇到重复键时抛 ArgumentException(“已存在相同键”)。先把数据 GroupBy 再 ToDictionary,或改用显式合并逻辑,才能避免该异常。
关键点: ToDictionary 假定键唯一,重复键是异常不是覆盖。
0550 以下代码输出什么?
难度: 实战
var outer = new[] { 1, 2, 3 };
var inner = new[] { 1, 1, 3 };
var query = outer.Join(inner, o => o, i => i, (o, i) => $"{o}-{i}");
Console.WriteLine(string.Join(" ", query));
- A.
1-1 1-1 3-3,每个 outer 元素与每个匹配的 inner 元素各产生一行 - B.
1-1 3-3 - C.
1-1 1-1 2-2 3-3 - D.
1-1 3-3 3-3
查看答案与解析
正确答案A
解析: Join 是笛卡尔积式的内连接:outer 的 1 命中 inner 的两个 1,产生两行;outer 的 3 命中一个 3,产生一行;outer 的 2 无匹配被丢弃。匹配对数决定结果行数。
关键点: Join 不做去重,一对多会产生多行。
0551 以下代码输出什么?
难度: 实战
var outer = new[] { "A", "B" };
var inner = new[] { "a", "b" };
var query = outer.Join(inner, o => o, i => i, (o, i) => o + i);
Console.WriteLine(query.Count());
- A.
2 - B.
4 - C.
1 - D.
0,Join 默认键比较是区分大小写的序号比较
查看答案与解析
正确答案D
解析: Join 默认使用 EqualityComparer<string>.Default,按序号且区分大小写,“A”与“a”不匹配,结果为 0 行。需要忽略大小写必须传入 StringComparer.OrdinalIgnoreCase。
关键点: 连接键的比较器默认区分大小写。
0552 以下代码输出什么?
难度: 实战
var outer = new[] { "A", "B", "C" };
var inner = new[] { "a", "b" };
var query = outer.Join(inner, o => o, i => i, (o, i) => o + i, StringComparer.OrdinalIgnoreCase);
Console.WriteLine(string.Join(" ", query));
- A.
Aa Bb Cc - B.
Aa Bb - C.
Aa Bb,忽略大小写后 A↔a、B↔b 匹配,C 无匹配 - D.
A B
查看答案与解析
正确答案C
解析: 传入忽略大小写的比较器后,“A”命中“a”、“B”命中“b”;“C”在内序列没有对应键,作为内连接元素被丢弃。注意 A 选项错在 C 不会凭空匹配。
关键点: 内连接只保留两边都有匹配的键。
0553 以下代码输出什么?
难度: 进阶
var outer = new List<int> { 1, 2 };
var inner = new List<int> { 1, 2, 3 };
var query = outer.Join(inner, o => o, i => i, (o, i) => o + i);
inner.Add(2);
Console.WriteLine(string.Join(" ", query));
- A.
2 4 - B.
2 4 4,枚举时内序列已含新增的 2,outer 2 命中两个 inner 2 - C.
2 4 3 - D.
2 4 5
查看答案与解析
正确答案B
解析: Join 延迟执行,枚举开始时才物化内序列并配对。此时 inner 已是 2:outer 1 命中 1(和 2),outer 2 命中两个 2(4、4),输出 2 4 4。
关键点: 内序列的物化发生在枚举开始,而非查询定义时。
0554 关于 Join 与 GroupJoin 结果形状的差异,哪项正确?
难度: 进阶
- A. Join 对每对匹配输出一条结果,无匹配的 outer 元素不出现;GroupJoin 对每个 outer 元素恰好输出一次,组可能为空
- B. 两者都只输出有匹配的 outer 元素
- C. GroupJoin 对每对匹配都输出一行
- D. Join 与 GroupJoin 结果完全相同
查看答案与解析
正确答案A
解析: Join 输出的是“匹配对”(一对多会产生多行);GroupJoin 按 outer 元素聚合匹配的 inner 元素,每个 outer 元素一行,即使没有匹配也会得到空组。GroupJoin 是“左外”形态的分组连接。
关键点: Join 的行数由匹配对数决定,GroupJoin 的行数等于 outer 元素数。
0555 以下代码输出什么?
难度: 实战
var outer = new[] { 1, 2, 3 };
var inner = new[] { 1, 3 };
var query = outer.GroupJoin(inner, o => o, i => i, (o, g) => $"{o}:{g.Count()}");
Console.WriteLine(string.Join(" ", query));
- A.
1:1 3:1 - B.
1:1 2:1 3:1 - C.
1:1 2:0 3:1,无匹配的 outer 元素也输出,组为空 - D.
1:1 2:0 3:0
查看答案与解析
正确答案C
解析: GroupJoin 保证每个 outer 元素都有一条结果:1 命中 1(组大小 1)、3 命中 3(组大小 1)、2 无匹配(空组,大小 0)。空组不是被丢弃,而是 Count 为 0。
关键点: GroupJoin 不会丢 outer 元素,空组用 Count()==0 识别。
0556 以下代码输出什么?
难度: 进阶
var outer = new List<int> { 1, 2 };
var inner = new List<int> { 1 };
var query = outer.GroupJoin(inner, o => o, i => i, (o, g) => $"{o}:{g.Sum()}");
inner.Add(1);
Console.WriteLine(string.Join(" ", query));
- A.
1:1 2:0 - B.
1:2 2:2 - C.
1:1 2:1 - D.
1:2 2:0,枚举开始时内序列被物化,两个 1 都计入 outer 1 的组
查看答案与解析
正确答案D
解析: GroupJoin 在枚举开始时把内序列物化成查找结构,此时 inner 已含两个 1:outer 1 的组为 1(和 2),outer 2 无匹配(和 0)。
关键点: 内序列在首次枚举时被完整缓冲,之后的变化不影响本次结果。
0557 关于 GroupJoin 为每个 outer 元素生成的 inner 组,哪项正确?
难度: 进阶
- A. 组只能枚举一次,第二次抛异常
- B. 组基于已物化的查找结构,可以多次安全枚举
- C. 组每次枚举都会重新执行整个查询
- D. 组是流式枚举,可能读到后续 outer 元素
查看答案与解析
正确答案B
解析: GroupJoin 先把内序列整体物化,再为每个 outer 元素生成指向该存储的组视图,因此组可以多次安全枚举(如先 Count 再遍历元素)。这不同于需要小心的一次性迭代器。
关键点: GroupJoin 的组是“已物化数据的视图”。
0558 以下代码输出什么?
难度: 实战
var n = 0;
var query = new[] { 1, 2, 3 }.GroupBy(x => { n++; return x % 2; });
Console.WriteLine(n);
Console.WriteLine(query.Count());
Console.WriteLine(n);
- A.
0/2/3,键选择器在枚举时执行 - B.
3/2/3 - C.
0/2/0 - D.
0/3/3
查看答案与解析
正确答案A
解析: 定义查询时键选择器未执行(n=0);Count() 驱动枚举,3 个元素各执行一次键选择器(n=3),得到 2 个组。键选择器与谓词一样,都是延迟求值。
关键点: 分组键的选择器在枚举时才运行。
0559 以下代码输出什么?
难度: 实战
var outer = new[] { "a", "b" };
var inner = new[] { "b", "a" };
var query = outer.Join(inner, o => o, i => i, (o, i) => o);
Console.WriteLine(string.Join(",", query));
- A.
b,a,按内序列顺序 - B.
a,b,结果自动排序 - C.
b,a,按匹配先后 - D.
a,b,外层主导:按 outer 元素顺序输出
查看答案与解析
正确答案D
解析: Join 的结果顺序由外层驱动:按 outer 元素的出现顺序,逐元素输出其全部匹配。outer 是 “a” 在前,因此结果为 a,b,与 inner 的顺序无关,也没有排序。
关键点: Join 输出顺序跟随 outer,想排序要显式 OrderBy。
0560 以下代码输出什么?
难度: 进阶
class BadComparer : IEqualityComparer<int>
{
public bool Equals(int x, int y) => x % 2 == y % 2;
public int GetHashCode(int obj) => obj;
}
var query = new[] { 1, 2, 3, 4, 5, 6 }.GroupBy(x => x, new BadComparer());
Console.WriteLine(query.Count());
- A.
2,Equals 按奇偶分组 - B.
3 - C.
6,哈希不一致导致“相等”元素落入不同桶,未归并;比较器必须保证 Equals 相等时哈希相同 - D. 抛
ArgumentException
查看答案与解析
正确答案C
解析: 该比较器 Equals 认为奇偶相同的键相等,但 GetHashCode 返回键本身,1、3、5 哈希不同、落入不同桶,永远不会被 Equals 比较,因此分组完全按哈希进行,得到 6 组。违反“Equals 相等 ⇒ GetHashCode 相等”的契约,行为不可预期。
关键点: 键比较器必须同时遵守相等与哈希一致性。