C# LINQ —— 集合操作的瑞士军刀
LINQ(Language Integrated Query)是 C# 最具代表性的特性之一——它把"查询"变成语言级语法,让你用统一的方式筛选、变换、聚合任何集合(数组、List、Dictionary、数据库表、XML)。学会 LINQ,你处理数据的效率会提升一个量级。
一、LINQ 是什么?
LINQ 在 C# 3(.NET 3.5,2007)引入,核心理念是"把数据库的查询能力搬到语言里"。它能做的事:
- 过滤:从集合里挑出满足条件的元素(
Where)。 - 投影/转换:把每个元素变成另一种形式(
Select)。 - 排序:按字段升/降序(
OrderBy)。 - 分组:按 key 分组(
GroupBy)。 - 聚合:求和、平均、最大、最小(
Sum、Average、Max)。 - 连接:类似 SQL join(
Join)。 - 存在检查:有没有/全部满足(
Any、All)。
LINQ 不只是一个语法糖——它建立在 IEnumerable<T> + 扩展方法 + Lambda + 延迟执行之上,是 C# 设计水平的体现。
二、两种写法
LINQ 有两种等价的写法:
- 方法语法(Fluent Syntax):链式调用
.Where(...).Select(...)。日常最常用。 - 查询语法(Query Syntax):类似 SQL 的
from ... where ... select。复杂 join 时更清晰。
两者功能等价,编译后生成的 IL 一样。本系列示例优先用方法语法,因为更接近现代 C# 风格。
三、过滤 Where 与投影 Select
using System.Linq;
int[] nums = { 5, 2, 8, 1, 9, 3, 7, 4, 6 };
// Where:过滤
var evens = nums.Where(n => n % 2 == 0);
// [2, 8, 4, 6]
var big = nums.Where(n => n > 5);
// [8, 9, 7, 6]
// Select:投影(转换每个元素)
var doubled = nums.Select(n => n * 2);
// [10, 4, 16, 2, 18, 6, 14, 8, 12]
var strings = nums.Select(n => $"数字 {n}");
var indexed = nums.Select((n, i) => $"第 {i}: {n}");
// 链式调用:filter -> transform
var result = nums
.Where(n => n > 3)
.Select(n => n * 10);
// [50, 80, 90, 70, 40, 60]Lambda 表达式 n => n % 2 == 0 是 LINQ 的灵魂——"输入 n,返回判断结果"。链式调用让代码声明式地描述"要什么",而不是"怎么做"。
四、排序 OrderBy / ThenBy
int[] nums = { 5, 2, 8, 1, 9 };
// OrderBy:升序
var asc = nums.OrderBy(n => n); // [1, 2, 5, 8, 9]
// OrderByDescending:降序
var desc = nums.OrderByDescending(n => n); // [9, 8, 5, 2, 1]
// 多级排序:ThenBy
var students = new List<Student>
{
new() { Name = "Tom", Grade = 3, Score = 85 },
new() { Name = "Bob", Grade = 3, Score = 90 },
new() { Name = "Amy", Grade = 2, Score = 88 },
};
var sorted = students
.OrderBy(s => s.Grade)
.ThenByDescending(s => s.Score);
// 按年级升序,同年级按分数降序
// Reverse:翻转
var rev = nums.Reverse(); // [9, 1, 8, 2, 5]注意:
- OrderBy 只能有一个,ThenBy 跟在后面做多级排序。
- OrderBy 是稳定排序——相等元素保持原顺序。
- Reverse 是简单翻转,不做任何比较。
- 自定义比较器:
OrderBy(n => n, new MyComparer())。
五、聚合与存在检查
int[] nums = { 1, 2, 3, 4, 5 };
// Count
int total = nums.Count(); // 5
int evens = nums.Count(n => n % 2 == 0); // 2
// Sum / Min / Max / Average
int sum = nums.Sum(); // 15
int max = nums.Max(); // 5
int min = nums.Min(); // 1
double avg = nums.Average(); // 3
// Aggregate:自定义聚合(类似 reduce/fold)
int product = nums.Aggregate((a, b) => a * b); // 120 = 1*2*3*4*5
string concat = nums.Aggregate("", (s, n) => s + n + ","); // "1,2,3,4,5,"
// Any / All
bool hasBig = nums.Any(n => n > 4); // True
bool allPos = nums.All(n => n > 0); // True
// First / FirstOrDefault / Single
int first = nums.First(); // 1
int firstBig = nums.First(n => n > 3); // 4
int notFound = nums.FirstOrDefault(n => n > 100); // 0(默认值)
int single = new[] { 42 }.Single(); // 42(序列唯一元素)几个易混点:
- First vs FirstOrDefault:前者找不到抛异常,后者返回默认值(推荐)。
- Single vs First:Single 要求序列有且仅有一个,否则抛异常。
- Any 比 Count 快:检查"有没有"用
Any(),不要Count() > 0。 - Aggregate:最通用的聚合,可表达 Sum/Min/Max,但通常用专门方法。
六、分组 GroupBy 与去重 Distinct
var orders = new List<Order>
{
new() { Customer = "Tom", Amount = 100 },
new() { Customer = "Tom", Amount = 200 },
new() { Customer = "Amy", Amount = 50 },
new() { Customer = "Amy", Amount = 80 },
new() { Customer = "Bob", Amount = 300 },
};
// GroupBy:按客户分组
var groups = orders.GroupBy(o => o.Customer);
foreach (var g in groups)
{
Console.WriteLine($"{g.Key}: {g.Sum(o => o.Amount)}");
// Tom: 300 / Amy: 130 / Bob: 300
}
// 分组后投影
var stats = orders
.GroupBy(o => o.Customer)
.Select(g => new
{
Customer = g.Key,
Total = g.Sum(o => o.Amount),
Count = g.Count(),
Average = g.Average(o => o.Amount)
})
.OrderByDescending(s => s.Total);
// Distinct:去重
var nums2 = new[] { 1, 2, 2, 3, 3, 3 };
var unique = nums2.Distinct(); // [1, 2, 3]
// Chunk(C# 8+):分块
var chunks = nums.Chunk(2); // [[1,2], [3,4], [5]]GroupBy 返回 IEnumerable<IGrouping<TKey, TElement>>,每个分组有 Key 和元素序列。配合 Select 投影出统计结果是经典模式。
七、查询语法(Query Syntax)
int[] nums = { 5, 2, 8, 1, 9, 3, 7 };
// 查询语法:类似 SQL(C# 特色)
var result = from n in nums
where n > 3
orderby n descending
select n * 10;
// [90, 80, 70, 50]
// 多 from:类似 SelectMany(扁平化)
var matrix = new[] { new[] { 1, 2 }, new[] { 3, 4 } };
var flat = from row in matrix
from n in row
select n;
// [1, 2, 3, 4]
// join:连接两个集合
var students = new[] { (Id: 1, Name: "Tom"), (Id: 2, Name: "Amy") };
var scores = new[] { (StuId: 1, Score: 90), (StuId: 2, Score: 85) };
var joined = from s in students
join sc in scores on s.Id equals sc.StuId
select new { s.Name, sc.Score };
// {Tom, 90}, {Amy, 85}
// group ... by
var grouped = from o in orders
group o by o.Customer into g
select new { Customer = g.Key, Total = g.Sum(x => x.Amount) };查询语法在多表 Join、SelectMany、Group 等复杂场景比方法语法清晰。日常 Where/Select/OrderBy 用方法语法更紧凑。
八、延迟执行(重要!)
LINQ 查询是延迟执行(lazy)的——定义时不跑,真正遍历/聚合时才跑:
// 延迟执行:LINQ 查询定义时不执行,遍历/聚合时才执行
var nums = new List<int> { 1, 2, 3, 4, 5 };
var evens = nums.Where(n => n % 2 == 0);
// 此时还没执行
nums.Add(6); // 修改源数据
nums.Add(8);
foreach (var n in evens)
Console.WriteLine(n);
// 2 4 6 8(看到了刚加的 6 和 8!)
// 强制立即执行:ToList / ToArray / ToDictionary / Count / Sum 等
var frozen = nums.Where(n => n % 2 == 0).ToList();
nums.Add(10);
// frozen 还是 [2, 4, 6, 8],不会变
// 性能陷阱:多次遍历同一个 LINQ 查询,重复执行
var query = nums.Where(ExpensiveFilter);
foreach (var n in query) { }
foreach (var n in query) { } // ExpensiveFilter 跑了两遍!
// 缓存结果
var cached = query.ToList();延迟执行的好处:
- 节省内存:不立即生成中间集合。
- 组合性强:多个操作可以合并一次遍历。
- 支持无限序列:
Naturals().Where(...).Take(10)。
延迟执行的坑:
- 源数据变化:遍历前修改源,结果会受影响。
- 多次遍历重复执行:每次 foreach 都重新跑查询。
- 解决:用
.ToList()/.ToArray()立即物化。
九、Join 与 SelectMany
var students = new[]
{
new { Id = 1, Name = "Tom", ClassId = 1 },
new { Id = 2, Name = "Amy", ClassId = 2 },
};
var classes = new[]
{
new { Id = 1, Name = "一班" },
new { Id = 2, Name = "二班" },
};
// Join:内连接
var joined = students.Join(
classes,
s => s.ClassId,
c => c.Id,
(s, c) => new { Student = s.Name, Class = c.Name }
);
// { Tom, 一班 }, { Amy, 二班 }
// SelectMany:扁平化(嵌套集合 -> 单层)
var teachers = new[]
{
new { Name = "T1", Students = new[] { "Tom", "Bob" } },
new { Name = "T2", Students = new[] { "Amy" } },
};
var all = teachers.SelectMany(t => t.Students);
// [Tom, Bob, Amy]
// GroupJoin:左连接(每个 left 元素 + 匹配的 right 集合)
var groupJoin = teachers.GroupJoin(
students,
t => t.Name,
s => s.ClassId.ToString(), // 假设有关系
(t, ss) => new { Teacher = t.Name, Count = ss.Count() }
);十、ToObject / ToDictionary / ToLookup
var nums = new[] { 1, 2, 3, 4, 5 };
// ToList / ToArray
List<int> list = nums.ToList();
int[] arr = nums.ToArray();
// ToDictionary:转字典(key 必须唯一)
var dict = nums.ToDictionary(n => n, n => n * n);
// {1:1, 2:4, 3:9, 4:16, 5:25}
// ToLookup:一对多(类似 Dictionary<K, IEnumerable<V>>)
var students = new[]
{
new { Name = "Tom", Grade = "A" },
new { Name = "Bob", Grade = "A" },
new { Name = "Amy", Grade = "B" },
};
var byGrade = students.ToLookup(s => s.Grade, s => s.Name);
foreach (var name in byGrade["A"]) // Tom, Bob
Console.WriteLine(name);
// ToHashSet
var set = nums.ToHashSet();十一、LINQ 处理对象集合示例
public record Order(string Customer, string Product, int Qty, decimal Price);
var orders = new List<Order>
{
new("Tom", "鼠标", 2, 99.9m),
new("Tom", "键盘", 1, 199m),
new("Amy", "鼠标", 1, 99.9m),
new("Bob", "显示器", 1, 999m),
new("Amy", "鼠标", 3, 99.9m),
};
// 1. 每个客户的总消费
var topCustomers = orders
.GroupBy(o => o.Customer)
.Select(g => new { Customer = g.Key, Total = g.Sum(o => o.Qty * o.Price) })
.OrderByDescending(x => x.Total)
.Take(3);
// [{Tom, 398.8}, {Amy, 399.6}, {Bob, 999}]
// 2. 最畅销商品
var topProduct = orders
.GroupBy(o => o.Product)
.Select(g => new { Product = g.Key, TotalQty = g.Sum(o => o.Qty) })
.OrderByDescending(x => x.TotalQty)
.First();
// {鼠标, 6}
// 3. 平均客单价
var avgOrder = orders.Select(o => o.Qty * o.Price).Average();
// 4. 谁买了超过 1 件商品
var bulkBuyers = orders
.Where(o => o.Qty > 1)
.Select(o => o.Customer)
.Distinct();
// [Tom, Amy]这就是 LINQ 的威力——上面这些查询如果用传统 foreach,代码量会多 3 倍,可读性差 10 倍。
十二、LINQ to Entities(数据库)
同样的 LINQ 语法可以用来查数据库——EF Core / EF / LINQ to SQL 把 LINQ 表达式翻译成 SQL:
// EF Core 中
using var db = new AppDbContext();
// 这段 LINQ 会翻译成 SQL:SELECT * FROM Users WHERE Age > 18 ORDER BY Name
var adults = db.Users
.Where(u => u.Age > 18)
.OrderBy(u => u.Name)
.ToList();
// JOIN 也行
var orders = db.Orders
.Where(o => o.Amount > 100)
.Join(db.Customers,
o => o.CustomerId,
c => c.Id,
(o, c) => new { c.Name, o.Amount })
.ToList();同一套语法,既能查内存集合,又能查数据库,这是 LINQ 设计的巨大成功。
十三、常见陷阱
- 多次遍历:LINQ 查询重复执行。缓存
.ToList()。 - 修改源数据:遍历中改集合抛异常。
- 空引用:First/Single 抛异常,用 FirstOrDefault/SingleOrDefault。
- 性能:大数据量下,LINQ 比 for 循环慢一些(委托开销)。性能瓶颈处可改 for。
- 翻译失败:EF Core LINQ 有些表达式翻译不成 SQL(运行时抛异常)。
- Count() == 0:用
Any()替代,避免遍历整个集合。
小结
LINQ 是 C# 的核心杀手锏。学会它,你处理集合的代码会变得声明式、简洁、强大。记住几个核心:Where 过滤、Select 转换、OrderBy 排序、GroupBy 分组、Any/All/First 检查、ToList 物化。下一篇我们看异常处理——让程序更健壮。
← 上一篇 C# 集合
下一篇 C# 异常处理 →