C# · #csharp#dotnet#linq

C# LINQ深入解析:从入门到性能优化

2015.03.15 C# 3 min 1.3k
// 目录 · contents

LINQ(Language Integrated Query)是 C# 3.0 引入的最重要特性之一,它将查询能力直接内嵌到语言本身,让操作集合、数据库、XML 变得统一而优雅。用了一段时间之后我发现,大多数人只会用 Where/Select 这几个基础操作,而对延迟执行、表达式树这些核心机制缺乏了解,导致踩了不少坑。这篇文章是我的一个系统梳理。

1. 查询语法 vs 方法语法

LINQ 有两种写法,底层完全等价:

1
2
3
4
5
6
7
8
9
10
11
// 查询语法(Query Syntax)
var result1 = from u in users
where u.Age > 18
orderby u.Name
select new { u.Name, u.Email };

// 方法语法(Method Syntax / Fluent API)
var result2 = users
.Where(u => u.Age > 18)
.OrderBy(u => u.Name)
.Select(u => new { u.Name, u.Email });

查询语法编译后会转换为方法语法,所以两者性能完全一致。我个人更习惯方法语法,链式调用更易于扩展,也更适合复杂查询的逐步调试。

查询语法有一个优势:joingroup by 写起来更直观:

1
2
3
4
5
6
7
var orderDetails = from o in orders
join p in products on o.ProductId equals p.Id
group o by p.Category into g
select new {
Category = g.Key,
TotalAmount = g.Sum(o => o.Amount)
};

2. 延迟执行(Deferred Execution)

这是 LINQ 最重要的特性,也是最容易踩坑的地方。

1
2
3
4
5
6
7
8
var query = users.Where(u => u.IsActive); // 此时没有执行任何查询

Console.WriteLine("查询还没开始");

foreach (var user in query) // 迭代时才真正执行
{
Console.WriteLine(user.Name);
}

陷阱一:多次枚举

1
2
3
4
5
6
7
8
9
var activeUsers = users.Where(u => u.IsActive);

int count = activeUsers.Count(); // 第一次遍历
var list = activeUsers.ToList(); // 第二次遍历 — 重复计算!

// 正确做法:物化一次
var activeUsersList = users.Where(u => u.IsActive).ToList();
int count2 = activeUsersList.Count;
var list2 = activeUsersList;

陷阱二:闭包捕获变量

1
2
3
4
5
6
7
8
9
10
11
12
13
var queries = new List<IEnumerable<User>>();
for (int i = 0; i < 3; i++)
{
// 错误:所有 query 都捕获了同一个 i,最终都是 i=3
queries.Add(users.Where(u => u.DeptId == i));
}

// 正确:在循环内创建局部变量
for (int i = 0; i < 3; i++)
{
int deptId = i;
queries.Add(users.Where(u => u.DeptId == deptId));
}

3. 常用操作符详解

3.1 聚合操作

1
2
3
4
5
6
7
8
var stats = users.Where(u => u.IsActive)
.GroupBy(u => u.Department)
.Select(g => new {
Department = g.Key,
Count = g.Count(),
AvgAge = g.Average(u => u.Age),
MaxSalary = g.Max(u => u.Salary)
});

3.2 集合操作

1
2
3
4
5
6
var list1 = new[] { 1, 2, 3, 4, 5 };
var list2 = new[] { 3, 4, 5, 6, 7 };

var union = list1.Union(list2); // { 1,2,3,4,5,6,7 }
var intersect = list1.Intersect(list2); // { 3,4,5 }
var except = list1.Except(list2); // { 1,2 }

3.3 分页

1
2
3
4
5
int page = 2, pageSize = 10;
var paged = users.OrderBy(u => u.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToList();

3.4 扁平化

1
2
3
4
5
6
// 每个 Department 有多个 User,获取所有 User
var allUsers = departments.SelectMany(d => d.Users);

// 带父对象
var userDeptPairs = departments
.SelectMany(d => d.Users, (dept, user) => new { dept.Name, user });

4. LINQ to Objects vs LINQ to SQL

两者语法一样,但执行方式完全不同:

LINQ to Objects LINQ to SQL / EF
数据来源 内存集合 数据库
Lambda 类型 Func<T, bool> Expression<Func<T, bool>>
执行位置 .NET 运行时 转换为 SQL 在数据库执行
过滤时机 全量加载后过滤 数据库层面过滤
1
2
3
4
5
// LINQ to Objects — 先把所有数据加载到内存再过滤(危险!)
var result = dbContext.Users.ToList().Where(u => u.Age > 18);

// LINQ to SQL — 生成 WHERE age > 18 的 SQL,只取需要的数据
var result = dbContext.Users.Where(u => u.Age > 18).ToList();

这个区别非常重要:第一种写法会把整张表加载到内存,百万行数据下直接 OOM。

5. 性能注意事项

避免 N+1 查询(EF 场景):

1
2
3
4
5
6
7
8
9
// 错误:每个订单都触发一次 SQL 查询用户信息
var orders = dbContext.Orders.ToList();
foreach (var order in orders)
{
Console.WriteLine(order.User.Name); // N 次查询
}

// 正确:用 Include 预加载
var orders = dbContext.Orders.Include(o => o.User).ToList();

使用 Any() 代替 Count() > 0

1
2
3
4
5
// 低效:需要遍历所有元素计数
if (users.Count() > 0) { }

// 高效:找到第一个就返回
if (users.Any()) { }

FirstOrDefault() 优于 Where().FirstOrDefault()

1
2
3
4
// 同等效果,但语义更清晰
var user = users.FirstOrDefault(u => u.Id == id);
// 等价于
var user = users.Where(u => u.Id == id).FirstOrDefault();

总结

LINQ 是 C# 最强大的特性之一,理解延迟执行和表达式树是用好它的前提:

  • 延迟执行:查询定义时不执行,迭代时才执行——避免重复枚举,注意闭包陷阱
  • 物化时机:在需要多次访问结果时,用 ToList()/ToArray() 物化一次
  • LINQ to SQL:过滤、排序、投影尽量在数据库层完成,不要 ToList() 之后再过滤
  • 可读性:复杂查询拆成多步,每步赋予有意义的变量名,比一条超长链式调用好维护
作者 · authorzt
发布 · date2015-03-15
篇幅 · length1.3k 字 · 3 min
许可 · licenseCC BY-SA 4.0
$ echo "comments" · 评论