C# 高级教程 - 10 高级 LINQ

基础教程中我们学习了 LINQ 的基本查询语法和方法链。真正的 LINQ 高手不仅会用 Where、Select 和 ToList,还能熟练运用高级分组连接、聚合操作、自定义扩展方法、以及理解延迟执行与 IQueryable 的底层原理。本文将深入 LINQ 的核心机制和最佳实践。

基础回顾速查

1
2
3
4
5
var products = GetProducts();
var query = products.Where(p => p.Price > 100)
.OrderBy(p => p.Category)
.ThenByDescending(p => p.Price)
.Select(p => new { p.Name, p.Price });

高级分组

GroupBy 进阶

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public record Product(string Name, string Category, decimal Price, int Stock);

var products = new List<Product>
{
new("电脑", "电子", 5000, 10),
new("手机", "电子", 3000, 20),
new("苹果", "食品", 5, 100),
new("香蕉", "食品", 3, 200),
};

// 多级分组
var grouped = products
.GroupBy(p => p.Category)
.Select(g => new
{
Category = g.Key,
Count = g.Count(),
TotalValue = g.Sum(p => p.Price * p.Stock),
AveragePrice = g.Average(p => p.Price),
Products = g.OrderByDescending(p => p.Price)
});

foreach (var group in grouped)
{
Console.WriteLine($"{group.Category}: {group.Count} 种, 总值 {group.TotalValue}");
}

// 自定义分组键
var byPriceRange = products.GroupBy(p =>
{
if (p.Price < 10) return "廉价";
if (p.Price < 1000) return "中等";
return "昂贵";
});

GroupJoin —— 嵌套分组连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public record Order(int Id, string Customer, decimal Amount);
public record Customer(string Name, string City);

var customers = new List<Customer>
{
new("张三", "北京"),
new("李四", "上海"),
};

var orders = new List<Order>
{
new(1, "张三", 100),
new(2, "张三", 200),
new(3, "李四", 150),
};

// GroupJoin:为每个客户生成该客户的所有订单
var customerOrders = customers.GroupJoin(
orders,
customer => customer.Name,
order => order.Customer,
(customer, orderList) => new
{
customer.Name,
OrderCount = orderList.Count(),
TotalAmount = orderList.Sum(o => o.Amount),
Orders = orderList
});

foreach (var co in customerOrders)
{
Console.WriteLine($"{co.Name}: {co.OrderCount} 单, 共 {co.TotalAmount} 元");
foreach (var order in co.Orders)
Console.WriteLine($" - 订单 #{order.Id}: {order.Amount} 元");
}

SelectMany —— 扁平化嵌套集合

1
2
3
4
5
6
7
8
9
10
11
12
13
var departments = new[]
{
new { Name = "技术部", Members = new[] { "张三", "李四", "王五" } },
new { Name = "市场部", Members = new[] { "赵六", "钱七" } },
};

// 扁平化为所有成员(附带部门信息)
var allMembers = departments.SelectMany(
dept => dept.Members,
(dept, member) => new { dept.Name, Member = member });

foreach (var m in allMembers)
Console.WriteLine($"{m.Name}: {m.Member}");

高级聚合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Aggregate —— 自定义累加器
var numbers = Enumerable.Range(1, 5);
int product = numbers.Aggregate((acc, n) => acc * n); // 1*2*3*4*5 = 120

// Aggregate 带种子值
int factorial = numbers.Aggregate(1, (acc, n) => acc * n); // 同样 120

// 复杂聚合:构建 CSV
string csv = products.Aggregate(
"名称,价格",
(acc, p) => $"{acc}\n{p.Name},{p.Price}");

// Zip —— 并行合并两个序列
var names = new[] { "A", "B", "C" };
var scores = new[] { 90, 85, 88 };
var combined = names.Zip(scores, (name, score) => $"{name}: {score}");
// ["A: 90", "B: 85", "C: 88"]

// Zip 三元组(.NET 6+)
var levels = new[] { "优", "良", "优" };
var triples = names.Zip(scores, levels);
// [(A, 90, 优), (B, 85, 良), (C, 88, 优)]

自定义 LINQ 扩展方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public static class LinqExtensions
{
// 过滤重复(自定义比较器)
public static IEnumerable<TSource> DistinctBy<TSource, TKey>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector)
{
var seen = new HashSet<TKey>();
foreach (var item in source)
if (seen.Add(keySelector(item)))
yield return item;
}

// 分页
public static IEnumerable<TSource> Page<TSource>(
this IEnumerable<TSource> source,
int pageIndex, int pageSize)
{
return source.Skip(pageIndex * pageSize).Take(pageSize);
}

// ForEach 扩展(链式调用)
public static IEnumerable<TSource> ForEach<TSource>(
this IEnumerable<TSource> source,
Action<TSource> action)
{
foreach (var item in source)
action(item);
return source;
}

// 异常安全迭代
public static IEnumerable<TSource> IgnoreErrors<TSource>(
this IEnumerable<TSource> source,
Action<Exception>? onError = null)
{
foreach (var item in source)
{
TSource? result = default;
try { result = item; }
catch (Exception ex)
{
onError?.Invoke(ex);
continue;
}
if (result is not null)
yield return result;
}
}

// TopN(高效取最大 N 个,避免全排序)
public static IEnumerable<TSource> TopN<TSource, TKey>(
this IEnumerable<TSource> source,
int count,
Func<TSource, TKey> keySelector,
Comparer<TKey>? comparer = null) where TKey : IComparable<TKey>
{
return source.OrderByDescending(keySelector, comparer ?? Comparer<TKey>.Default)
.Take(count);
}
}

// 使用
products.Where(p => p.Stock > 0)
.ForEach(p => Console.WriteLine(p.Name))
.Page(0, 10)
.ToList();

延迟执行 vs 立即执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
IEnumerable<int> GetNumbers()
{
Console.WriteLine("生成器执行了");
yield return 1;
yield return 2;
yield return 3;
}

// 延迟执行:没有输出
var query = GetNumbers().Where(n => n > 1);

// 迭代时实际执行
foreach (var n in query) // 输出"生成器执行了"一次,而非多次
Console.WriteLine(n);

// 立即执行:ToList/ToArray/Count/First/Any
var list = GetNumbers().ToList(); // 这里执行

陷阱:多次枚举

1
2
3
4
5
6
7
8
var expensiveQuery = products.Where(p => ExpensiveCheck(p));
int count1 = expensiveQuery.Count(); // 执行一次
int count2 = expensiveQuery.Count(); // 再次执行!ExpensiveCheck 又跑了一遍

// 正确做法
var evaluated = expensiveQuery.ToList();
int safeCount1 = evaluated.Count;
int safeCount2 = evaluated.Count;

IQueryable 与表达式树

1
2
3
4
5
6
7
8
9
10
// IEnumerable vs IQueryable —— 关键区别
IEnumerable<Product> enumerable = products;
IQueryable<Product> queryable = dbContext.Products; // EF Core 查询

// IEnumerable:内存中执行
var cheapProducts = enumerable.Where(p => p.Price < 100).ToList();

// IQueryable:构建表达式树,传给 SQL 提供器翻译为 SQL
var expensiveProducts = queryable.Where(p => p.Price > 1000).ToList();
// → 翻译为: SELECT * FROM Products WHERE Price > 1000

IQueryable 的内部结构

1
2
3
4
5
6
7
8
9
10
11
// Expression:查询的抽象语法树
// Provider:将表达式树翻译为具体查询(SQL、OData 等)

IQueryable<Product> query = dbContext.Products
.Where(p => p.Price > 100)
.OrderBy(p => p.Name)
.Take(10);

// 查看表达式树
Expression expressionTree = query.Expression;
// Provider 会在 GetEnumerator 时翻译并执行

自定义 IQueryable 提供器

一般不需要自己实现完整的 IQueryProvider,但理解原理有助于调试和性能优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
// EF Core 中的常见陷阱
// 错误:条件拆分导致 SQL 无法优化
var q1 = dbContext.Products.Where(p => condition1);
var q2 = q1.Where(p => condition2); // 翻译为一个 SQL
var result = q2.ToList(); // WHERE condition1 AND condition2

// 错误:在 Where 中调用 C# 方法(无法翻译)
var today = DateTime.Today;
var bad = dbContext.Orders.Where(o => IsWeekend(o.Date)).ToList(); // 运行时异常!

// 正确:使用可翻译表达式
var good = dbContext.Orders.Where(o => o.Date.DayOfWeek == DayOfWeek.Saturday
|| o.Date.DayOfWeek == DayOfWeek.Sunday).ToList();

LINQ 性能优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 1. 优先使用 Any 而不是 Count > 0
bool hasItems = items.Any(); // 找到一个就返回,O(1)
bool hasMany = items.Count() > 0; // 遍历全部,O(n)

// 2. 使用 ToDictionary 建立快速查找表
var orderDict = orders.ToDictionary(o => o.Id);
if (orderDict.TryGetValue(42, out var order)) { }

// 3. 避免 Where().Select().OrderBy().Skip().Take() 的重复计算
var query = products.Where(p => p.Stock > 0)
.OrderBy(p => p.Price)
.Select(p => new { p.Name, p.Price });
// 只迭代一次
foreach (var p in query) { }

// 4. 用 HashSet 优化 Contains
var validIds = new HashSet<int>(validIdList);
var filtered = allItems.Where(item => validIds.Contains(item.Id));

// 5. 大数据集避免 OrderBy 后 Skip 大量数据
// 差:products.OrderBy(p => p.Price).Skip(10000).Take(10) — 需要排序全部
// 好:改为 SQL 层面分页或使用数据库游标

总结:LINQ 是 C# 中最具生产力的特性之一。高级分组连接、自定义聚合、扩展方法的组合能力使其不仅能处理简单查询,还能优雅地表达复杂的数据变换逻辑。理解延迟执行与 IQueryable 的差异是避免性能陷阱的关键。在 .NET 中,熟练掌握 LINQ 意味着可以用更少的代码实现更多的功能,同时保持代码的可读性和可维护性。

ByteFisher
分享编程技术 · 记录钓鱼乐趣
扫码关注
▸ 扫码关注 ◂
分享: