本次复习包含12道编程题目,旨在帮助你系统性地回顾和巩固 LINQ 的核心概念、常见陷阱及工程应用模式。题目按难度递进,覆盖了从基础理解到复杂建模的各个层面。
题目数量:12 题
难度分布:
- 热身理解题:3 题
- 核心建模题:6 题
- 综合工程题:3 题
一、热身理解题
① 延迟执行 + 副作用理解
var logs = new List<string>();
var query = users
.Where(u =>
{
logs.Add($"Checking {u.Name}");
return u.Age >= 30;
});
logs.Add("Before enumerate");
foreach (var u in query)
{
}
logs.Add("After enumerate");
请思考以下问题:
logs 的最终内容顺序是什么?
- 如果删除
foreach 循环,这段代码还会往 logs 列表里添加内容吗?
- 这段代码在真实工程项目中是否合理?为什么?
✅ 标准答案
Before enumerate
Checking A
Checking B
...
After enumerate
Where 操作符遵循 延迟执行 原则。
- 副作用(这里指添加日志)发生在 枚举阶段。
- 没有枚举操作,Lambda 表达式根本不会执行。
工程判断
❌ 不合理
- LINQ 的 Lambda 表达式 不应该包含副作用,如写日志、修改外部状态等。
- 日志记录或状态修改等操作,应明确放在
foreach 循环内或其它流程控制语句中。
② 多次枚举的隐性成本
var query = users.Where(u => u.Age > 18);
bool hasAdult = query.Any();
int count = query.Count();
请思考以下问题:
Where 条件会被计算执行几次?
- 如果
users 来源于数据库查询,这段代码存在什么风险?
- 如何改写代码使其更安全?
✅ 标准答案
- 两次。
- 会导致两次独立的数据库查询,增加不必要的 IO 开销和网络往返,可能带来性能问题和数据不一致的风险。
- 通过
ToList() 或 ToArray() 将结果固化,避免多次枚举:
var list = users.Where(u => u.Age >= 30).ToList();
var hasAny = list.Any();
var count = list.Count;
要点
多次枚举 = 多次 IO / 多次计算
③ IEnumerable vs List 的边界
IEnumerable<User> q = users.Where(u => u.IsActive);
var list = q.ToList();
请思考以下问题:
- 哪一行代码真正触发了查询执行?
- 如果在执行
ToList() 之后修改了原始的 users 集合,list 中的内容会随之变化吗?
- 如果调整
q 这行代码定义的顺序(比如先定义 q,后修改 users),会影响最终 list 的结果吗?
✅ 标准答案
ToList():立即执行查询并固化结果。
list 不会变化,因为它已经是独立于源集合 users 的一个新列表快照。
- 会影响。因为
q 是延迟执行的查询定义,在调用 ToList() 时才会基于当前的 users 状态进行计算。这种与基础与综合思想相关的“定义与执行分离”特性,是理解 LINQ 延迟执行的关键。
二、核心建模题(中级 LINQ 的应用核心)
④ XML 日志分析
给定 XML 结构:
<logs>
<log level="Info" module="Auth" />
<log level="Error" module="DB" />
<log level="Error" module="Auth" />
<log level="Warn" module="API" />
</logs>
假设你已将其解析为以下对象列表:
class LogEntry
{
public string Level { get; set; }
public string Module { get; set; }
}
要求:
- 按
Level 分组。
- 统计每个 Level 出现的数量。
- 标记该 Level 组内是否存在
Error 级别的日志。
- 输出结构为
{ Level, Count, HasError } 的匿名对象列表。
var query = logEntries.GroupBy(l => l.Level)
.Select(g => new {
Level = g.Key,
Count = g.Count(),
HasError = g.Key == “Error”
}).ToList();
⑤ 配置文件合并(Union / DistinctBy)
class ConfigItem
{
public string Key { get; set; }
public string Value { get; set; }
}
- 本地配置:
localConfigs
- 远程配置:
remoteConfigs
要求:
- 合并两份配置列表。
- 以
Key 作为唯一标识符。
- 本地配置优先(即 Key 冲突时,采用本地配置的值)。
- 输出最终去重后的配置列表。
// 1. 顺序至关重要:把“优先”的集合放在连接操作的前面
var finalConfigs = localConfigs
.Concat(remoteConfigs) // 先把两个列表连起来,本地在前,远程在后
.DistinctBy(c => c.Key) // 遇到重复的 Key,保留先看到的(即本地的)
.ToList();
⑥ 权限系统建模(SelectMany)
class Role
{
public string Name { get; set; }
public List<string> Permissions { get; set; }
}
要求:
- 获取所有角色拥有的、去重后的权限列表。
- 按权限名称排序。
- 输出
List<string>。
var permissionList = roles.SelectMany(r=>r.Permissions).Distinct().OrderBy(p=>p).ToList();
⑦ 操作审计流(TakeWhile / SkipWhile)
给定按时间排序的操作记录:
class Audit
{
public DateTime Time { get; set; }
public bool IsValid { get; set; }
}
要求:
- 获取连续合法的操作序列(直到遇到第一个非法操作为止)。
- 跳过所有合法操作,获取剩余部分(从第一个非法操作开始及之后的所有记录)。
- 思考:如果使用
Where 操作符来实现,会有什么不同?
1. 取「连续合法」的操作(直到第一个非法)
var validSequence = audits.TakeWhile(a => a.IsValid);
TakeWhile:
- 从 第一个元素开始 判断。
- 条件第一次失败就整体停止,后续元素即使满足条件也不再包含。
- 非常适合:
2. 跳过所有合法操作,取剩余非法部分
var invalidTail = audits.SkipWhile(a => a.IsValid);
- 跳过「前缀合法区」。
- 返回 第一个非法操作 + 之后的所有操作。
3. 如果用 Where 会怎样?
audits.Where(a => a.IsValid);
❌ 不等价,核心区别:
| TakeWhile / SkipWhile |
Where |
| 有顺序语义,依赖于集合的遍历顺序 |
无顺序语义,进行全局筛选 |
| 遇错即停(TakeWhile),或跳过前缀(SkipWhile) |
全表扫描,返回所有符合条件的元素 |
| 表达“阶段”、“前缀”等流程概念 |
表达“筛选”集合的概念 |
⚠️ TakeWhile / SkipWhile 是流程算子,Where 是集合算子。
⑧ 日志分页(真实分页)
class Log
{
public DateTime Time { get; set; }
public string Message { get; set; }
}
要求:
- 按时间倒序排列。
- 实现获取第
pageIndex 页(从0开始),每页 pageSize 条记录的分页查询。
- 思考:分页前为什么必须进行排序?
var page = logs
.OrderByDescending(l => l.Time)
.Skip(pageIndex * pageSize)
.Take(pageSize)
.ToList();
为什么必须先排序?
❗ 如果顺序不稳定:
- 第 1 页和第 2 页的数据 可能重复或丢失。
- 用户界面翻页时会出现数据“闪烁”或错乱。
👉 分页的前提是结果集拥有稳定、可预测的顺序。
排序是分页逻辑不可或缺的一部分,不是附加条件。
⑨ 业务异常检测(Any / All)
class ServiceStatus
{
public string Service { get; set; }
public bool IsHealthy { get; set; }
}
要求:
- 判断是否存在不健康的服务。
- 判断是否全部服务都健康。
- 思考:
Any 和 All 操作符的“短路”行为在此场景下的意义。
bool hasUnhealthy = statuses.Any(s => !s.IsHealthy);
bool allHealthy = statuses.All(s => s.IsHealthy);
Any:遇到 第一个 满足条件(true)的元素即返回 true。
All:遇到 第一个 不满足条件(false)的元素即返回 false。
⚠️ 工程意义:
- 监控检查:快速发现异常。
- 告警触发:无需遍历完所有数据即可决策。
- 数据库 Exists 查询:可高效转换为
EXISTS 语句。
👉 在只需判断“是否存在”的场景下,使用 Any 或 All 比 Count() > 0 或 Count() == total 更快、更安全(避免了不必要的计数)。
三、综合工程题
⑩ 模块使用统计(GroupBy + 聚合)
class ModuleUsage
{
public string Module { get; set; }
public int Duration { get; set; }
}
要求:
- 按
Module 分组。
- 统计每个模块的总使用时长。
- 统计每个模块的调用次数。
- 标记该模块是否存在单次调用时长超过 1000ms 的记录。
- 输出可直接绑定到 UI 视图的扁平化 ViewModel 列表。
✅ 标准答案
var vm = usages
.GroupBy(u => u.Module)
.Select(g => new ModuleUsageVm
{
Module = g.Key,
TotalDuration = g.Sum(x => x.Duration),
CallCount = g.Count(),
HasLongCall = g.Any(x => x.Duration > 1000)
})
.ToList();
GroupBy 构建数据结构。
- 聚合函数(
Sum, Count, Any)生成业务结论。
- 输出扁平化的 ViewModel,而不是
IGrouping 对象。
UI 视图应该绑定结论数据,而不是原始的分组结构。
⑪ 配置变更差异分析(Except / Intersect)
假设有以下配置类:
class Config
{
public string Key { get; set; }
public string Value { get; set; }
}
1. 新增了哪些配置?
var added = newConfigs
.Select(c => c.Key)
.Except(oldConfigs.Select(c => c.Key));
2. 删除了哪些配置?
var removed = oldConfigs
.Select(c => c.Key)
.Except(newConfigs.Select(c => c.Key));
3. Key 相同但 Value 发生了改变?
var changed =
from oldC in oldConfigs
join newC in newConfigs on oldC.Key equals newC.Key
where oldC.Value != newC.Value
select new { oldC.Key, oldC.Value, newC.Value };
4. 哪些地方不能只靠 Except?
❌ Except 只能判断某个元素 “是否存在” 于另一个集合中。
它无法判断:
- 对象属性的具体变化(如
Value 字段的修改)。
- 复杂对象的状态演进。
- 差异的具体来源和内容。
Except 看“有没有”,Join 看“变没变”。
⑫ LINQ 设计题
问题:
在一个 WPF 项目中,你需要从 ObservableCollection<Data> 派生出多个用于显示统计信息的 ViewModel。
- 哪些场景下适合使用 LINQ?
- 哪些场景下必须使用
ToList() 或 ToArray()?
- 哪些场景下你会坚决避免使用 LINQ?
请写出你的判断理由,而非具体代码。
✔ 适合使用 LINQ 的地方
- 派生 ViewModel:对源数据进行筛选、转换以生成显示数据。
- 统计计算:求和、计数、平均值、分组等只读聚合操作。
- 数据筛选与投影:根据条件选择特定数据或转换其形状。
- 核心原则:不改动原始数据的声明式、只读计算。
✔ 必须使用 ToList() 或 ToArray() 的地方
- 结果被多次使用:避免因延迟执行导致的多次计算或查询。
- 作为 UI 控件的绑定源(如
ItemsSource):WPF/Silverlight 等框架通常需要稳定的集合(如 List<T>)而非 IEnumerable<T> 进行绑定,且能防止源集合变化导致绑定失效。
- 固化快照:需要在某个时间点保存数据的完整状态,防止后续对源集合的修改影响当前视图。
关于集合操作的最佳实践与陷阱,你可以在我们的技术文档专区找到更多深度解析。
❌ 坚决避免使用 LINQ 的地方
- 操作包含副作用:如修改全局状态、写入日志、发送网络请求等。
- 复杂的条件分支(if/else)或流程控制:传统的
foreach 循环和 if 语句在逻辑清晰度上通常更胜一筹。
- 实现状态机或复杂的多步骤流程:LINQ 是查询语言,不擅长描述有状态的流程。
- 性能极度敏感的热点路径:虽然 LINQ 性能在大多数情况下足够好,但在纳秒级优化的场景下,手写循环可能仍有微优势(需经性能测评确认)。
⚠️ 一句话总原则:
LINQ 是用于数据查询和转换的“表达式”语言,不是用于描述任意业务“流程”的命令式语言。
希望这套C#/.Net领域的LINQ综合练习题能帮助你查漏补缺,将理论知识转化为解决实际工程问题的能力。如果你在练习中遇到任何问题或有独到的见解,欢迎到云栈社区与更多开发者交流讨论。