C# LINQ & Collections Deep Dive
IEnumerable vs IQueryable, query syntax vs method syntax, deferred execution, common operators, and performance pitfalls.
Two Syntaxes, One Engine
using System.Linq;
var orders = new List<Order> { /* ... */ };
// 方法语法(最常见)
var recent = orders
.Where(o => o.CreatedAt >= DateTime.Now.AddDays(-7))
.OrderByDescending(o => o.Amount)
.Take(10)
.Select(o => o.Id)
.ToArray();
// 查询语法(复杂 join 时更直观)
var q = from o in orders
where o.CreatedAt >= DateTime.Now.AddDays(-7)
orderby o.Amount descending
select o.Id;
Common Operators
| 操作符 | 作用 | 复杂度/备注 |
|---|---|---|
| Where | 过滤 | O(n) 流式 |
| Select | 投影映射 | 逐元素 |
| SelectMany | 打平嵌套集合 | 子列表拼接 |
| OrderBy/ThenBy | 排序(稳定) | O(n log n),需 ToList 才执行 |
| GroupBy | 分组 | 立即执行聚合 |
| Distinct | 去重 | 需哈希 |
| First/FirstOrDefault | 取第一个 | 勿对空集用 First |
| Any/All | 存在/全部判断 | Any 找到即停 |
| ToDictionary | 转字典 | 键重复抛异常 |
| Sum/Min/Max/Average | 聚合 | 立即执行 |
// 分组聚合:按用户统计订单总额
var stats = orders
.GroupBy(o => o.UserId)
.Select(g => new { UserId = g.Key, Total = g.Sum(o => o.Amount), Count = g.Count() });
// SelectMany:用户 -> 订单扁平化
var pairs = users.SelectMany(u => u.Orders, (u, o) => new { u.Name, o.Id });
// 字典索引(O(1) 查找)
var byId = orders.ToDictionary(o => o.Id);
if (byId.TryGetValue(42, out var target)) { /* ... */ }
// Left join
var joined = from u in users
join o in orders on u.Id equals o.UserId into gj
from o in gj.DefaultIfEmpty()
select new { u.Name, OrderId = o?.Id };
Deferred Execution and Pitfalls
var query = orders.Where(o => o.Amount > 100); // 尚未执行
orders.Add(new Order { Amount = 500 }); // 数据源改变
query.Count(); // 501 那条也算进去:query 绑定的是序列本身
// 多次枚举 = 多次计算:缓存一次结果
var list = query.ToList();
list.Count(); list.Count(); // 只算一次
// 无限序列必须 Take/Distinct 截断
// 副作用函数不要用在 Where/Select 中
// IQueryable 的表达式会翻译成 SQL:C# 本地函数翻译不了
int localFn(int x) => x * 2;
// db.Orders.Where(o => localFn(o.Amount) > 10); // ✗ EF 无法翻译
// db.Orders.Where(o => o.Amount * 2 > 10); // ✓
Memory: IEnumerable holds nothing — materialize with ToList/ToArray when you reuse the result; keep it lazy when the consumer streams once.
LINQ in EF Core
// IQueryable:表达式树翻译为 SQL 在数据库执行
using var db = new ShopContext();
var page = await db.Orders
.Where(o => o.Status == OrderStatus.Paid)
.OrderByDescending(o => o.CreatedAt)
.Skip(20).Take(10) // LIMIT 10 OFFSET 20
.ToListAsync();
// Include = 预加载关联(防 N+1)
var users = await db.Users.Include(u => u.Orders).ToListAsync();
// 两种执行模型
IEnumerable<T>: 内存计算(先 AsEnumerable 拉回)
IQueryable<T>: 翻译成 SQL(翻译不了抛运行时异常)
Use IQueryable as far as possible (filtering, paging, projection on the server); materialize to IEnumerable only for in-memory post-processing. A Where that cannot translate forces client-side evaluation or an exception.
两种语法,同一引擎
using System.Linq;
var orders = new List<Order> { /* ... */ };
// 方法语法(最常见)
var recent = orders
.Where(o => o.CreatedAt >= DateTime.Now.AddDays(-7))
.OrderByDescending(o => o.Amount)
.Take(10)
.Select(o => o.Id)
.ToArray();
// 查询语法(复杂 join 时更直观)
var q = from o in orders
where o.CreatedAt >= DateTime.Now.AddDays(-7)
orderby o.Amount descending
select o.Id;
常用操作符
| 操作符 | 作用 | 复杂度/备注 |
|---|---|---|
| Where | 过滤 | O(n) 流式 |
| Select | 投影映射 | 逐元素 |
| SelectMany | 打平嵌套集合 | 子列表拼接 |
| OrderBy/ThenBy | 排序(稳定) | O(n log n),需 ToList 才执行 |
| GroupBy | 分组 | 立即执行聚合 |
| Distinct | 去重 | 需哈希 |
| First/FirstOrDefault | 取第一个 | 勿对空集用 First |
| Any/All | 存在/全部判断 | Any 找到即停 |
| ToDictionary | 转字典 | 键重复抛异常 |
| Sum/Min/Max/Average | 聚合 | 立即执行 |
// 分组聚合:按用户统计订单总额
var stats = orders
.GroupBy(o => o.UserId)
.Select(g => new { UserId = g.Key, Total = g.Sum(o => o.Amount), Count = g.Count() });
// SelectMany:用户 -> 订单扁平化
var pairs = users.SelectMany(u => u.Orders, (u, o) => new { u.Name, o.Id });
// 字典索引(O(1) 查找)
var byId = orders.ToDictionary(o => o.Id);
if (byId.TryGetValue(42, out var target)) { /* ... */ }
// Left join
var joined = from u in users
join o in orders on u.Id equals o.UserId into gj
from o in gj.DefaultIfEmpty()
select new { u.Name, OrderId = o?.Id };
延迟执行与陷阱
var query = orders.Where(o => o.Amount > 100); // 尚未执行
orders.Add(new Order { Amount = 500 }); // 数据源改变
query.Count(); // 501 那条也算进去:query 绑定的是序列本身
// 多次枚举 = 多次计算:缓存一次结果
var list = query.ToList();
list.Count(); list.Count(); // 只算一次
// 无限序列必须 Take/Distinct 截断
// 副作用函数不要用在 Where/Select 中
// IQueryable 的表达式会翻译成 SQL:C# 本地函数翻译不了
int localFn(int x) => x * 2;
// db.Orders.Where(o => localFn(o.Amount) > 10); // ✗ EF 无法翻译
// db.Orders.Where(o => o.Amount * 2 > 10); // ✓
内存:IEnumerable 本身不持有数据——结果要复用就 ToList/ToArray 物化,只流式消费一次就保持延迟。
EF Core 中的 LINQ
// IQueryable:表达式树翻译为 SQL 在数据库执行
using var db = new ShopContext();
var page = await db.Orders
.Where(o => o.Status == OrderStatus.Paid)
.OrderByDescending(o => o.CreatedAt)
.Skip(20).Take(10) // LIMIT 10 OFFSET 20
.ToListAsync();
// Include = 预加载关联(防 N+1)
var users = await db.Users.Include(u => u.Orders).ToListAsync();
// 两种执行模型
IEnumerable<T>: 内存计算(先 AsEnumerable 拉回)
IQueryable<T>: 翻译成 SQL(翻译不了抛运行时异常)
尽量让 IQueryable 走到底(过滤、分页、投影都在数据库完成);只有需要本地后处理时才 AsEnumerable 拉回内存。翻译不了的 Where 会落到客户端求值或直接抛异常。