2.6 集合与 LINQ
集合承载业务数据,LINQ 用声明式方式表达查询。它既用于内存集合,也用于 EF Core 数据库查询,但两者执行位置和性能特征不同。
学习目标
- 能选择
List<T>、Dictionary<TKey,TValue>、HashSet<T>等常用集合。 - 能使用 LINQ 完成筛选、排序、投影、分组和聚合。
- 能区分立即执行和延迟执行。
- 能理解内存 LINQ 与 EF Core LINQ 的差异。
应用场景
- 从任务列表中筛选未完成任务。
- 根据标签、优先级或日期分组统计。
- 把实体转换为返回给前端的 DTO。
- 在服务层中组织业务计算。
核心概念
| 操作 | 用途 | 示例 |
|---|---|---|
Where | 筛选 | 只要未完成任务 |
OrderBy | 排序 | 按截止日期升序 |
Select | 投影 | 实体转 DTO |
GroupBy | 分组 | 按状态统计数量 |
Any | 判断是否存在 | 是否有逾期任务 |
案例:任务统计
var todos = new List<TodoItem>
{
new(1, "学习 C#", false, "学习"),
new(2, "完成项目切片", true, "项目"),
new(3, "复习 EF Core", false, "学习"),
};
var summary = todos
.GroupBy(todo => todo.Category)
.Select(group => new TodoCategorySummary(
group.Key,
Total: group.Count(),
Completed: group.Count(todo => todo.IsCompleted)))
.OrderByDescending(item => item.Total)
.ToList();
public sealed record TodoItem(int Id, string Title, bool IsCompleted, string Category);
public sealed record TodoCategorySummary(string Category, int Total, int Completed);
示例:EF Core 查询中的 LINQ
var todos = await dbContext.Todos
.AsNoTracking()
.Where(todo => !todo.IsCompleted)
.OrderBy(todo => todo.DueDate)
.Select(todo => new TodoListItemDto(todo.Id, todo.Title, todo.DueDate))
.Take(20)
.ToListAsync(cancellationToken);
这段 LINQ 会被 EF Core 翻译成 SQL。不要在 ToListAsync 之前调用无法翻译的 C# 方法,否则可能导致查询失败或提前加载大量数据。
重点难点
IEnumerable<T>通常表示内存枚举,IQueryable<T>通常表示可被查询提供程序翻译。ToList()、Count()、First()会触发立即执行。- 多次枚举可能重复执行查询或重复计算。
- 数据库分页必须在数据库侧完成,不要先取全量再分页。
常见误区
| 误区 | 推荐做法 |
|---|---|
| 写出长链 LINQ 但没人读得懂 | 适当拆中间变量并命名业务含义 |
| 在 EF 查询中调用本地方法 | 使用可翻译表达式或先明确切到内存 |
以为 Where 立刻执行 | 理解延迟执行,知道什么时候触发 |
练习
- 按任务状态统计数量。
- 找出每个分类下最近创建的 3 个任务。
- 把实体列表投影为只包含
Id、Title、IsCompleted的 DTO。