当数据越来越多,我们就会渴望一种“像说人话一样处理数据”的方式:筛选、映射、分组、排序、连接…… C# 给我们的答案是 LINQ(Language Integrated Query):把数据操作的语义“嵌入语言”,让我们在编译期就能得到类型安全与智能提示,同时保持代码可读、可组合、可测试。

下面是一个简单的 LINQ 示例,它展示了如何使用 LINQ 来筛选、映射和排序数据。
|using System; using System.Linq; class Program { static void Main() { int[] nums = { 1, 2, 3, 4, 5, 6 }; var query = nums .Where(n => n % 2 == 0) // 筛选偶数 .Select(n => n * n) // 平方 .OrderByDescending(n => n); // 倒序 foreach (var x in query) { Console.WriteLine(x); // 36, 16, 4 } } }
我们可以把 LINQ 想象成一条工厂里的传送带:每个数据像一件原材料,从左到右经过不同的加工环节。 比如刚才的例子,数字们先被筛选出偶数,再被加工成平方,最后还要排个队,谁大谁先出来。
LINQ 的魅力就在于这种“搭积木”式的组合,每一步都清晰独立,而且只有当我们真正去“看结果”(比如用 foreach 遍历)时,整个流水线才会启动,把所有加工一步到位。
在 C# 里,LINQ 提供了两种不同的表达方式,其实就像我们用两种语言讲同一个故事。 第一种叫“方法语法”,它就像我们用积木一块块搭建,把各种操作(比如筛选、排序、转换)用点(.)连起来,像流水线一样一环接一环。 第二种叫“查询语法”,它更像 SQL 语句,读起来有点像在和数据库对话:先说从哪拿数据,再说怎么筛选、排序、最后要什么结果。
这两种写法本质上是等价的,背后做的事情一样,只是表达方式不同。我们可以根据自己的习惯和场景选择用哪一种,就像有时候喜欢用普通话,有时候用家乡话,怎么顺手怎么来。
|using System; using System.Linq; class Program { static void Main() { string[] words = { "pear", "apple", "banana", "plum" }; // 方法语法(链式) var q1 = words.Where(w => w.Length >
在多数团队里,大家更常使用方法语法,因为它更容易与条件逻辑/函数组合在一起。查询语法在连接(join)与分组(group)时有时更直观。选一个你读得最舒服的即可。
在 LINQ 的世界里,有一个很有趣的现象,叫做“延迟执行”。我们可以把它想象成点菜:当我们用 LINQ 写下各种操作(比如 Where、Select)时,其实只是把菜名写在菜单上,厨房还没真正开始做菜。 只有当我们真的“上菜”——也就是用 foreach 遍历、或者调用 ToList、ToArray 这些方法时,LINQ 才会把之前的操作一口气执行完,把结果端上来。
举个例子,如果我们写了一个 Where 过滤条件,LINQ 不会立刻去筛选数据,而是等到我们真正需要结果的时候才动手。这种方式有个好处,就是可以把多个操作像流水线一样串起来,最后一次性处理,效率很高。 不过,有时候我们希望马上得到一个“快照”,比如想要立刻把结果保存下来,这时就需要用到“立刻执行”的方法。
常见的有 ToList、ToArray(把结果变成列表或数组),还有 Count(统计数量)、Sum(求和)、Any(判断有没有元素)、First(取第一个)等等。这些方法一调用,LINQ 就会立刻把之前的操作全部执行,结果马上就能拿到。
所以,延迟执行让我们可以灵活组合各种操作,但如果想要结果不再受后续数据变化影响,就要用这些立刻执行的方法,把结果“定格”下来。
|using System; using System.Linq; var list = new System.Collections.Generic.List<int> { 1, 2, 3 }; var q = list.Where(x => x > 1); // 还未执行 list.Add
延迟执行让组合更强,但也常带来“源数据变了、结果也跟着变”的惊讶。要稳定快照就用 ToList/ToArray。
让我们用一个温馨的小故事,把 LINQ 的常用操作串起来讲一遍。想象一下,我们班上有几位同学,每个人都有自己的兴趣爱好。现在,我们要用 LINQ 来帮忙做一些有趣的“数据魔法”: 比如筛选出年龄大于等于 17 岁的同学,按照年龄和名字排序,提取他们的名字;再比如,把所有同学的兴趣标签都收集起来, 去掉重复的,看看班级里一共有多少种兴趣;还可以模拟一下“翻页”,比如跳过第一个同学,取接下来的两位,看看他们是谁。通过这些实际的小任务,我们一步步体会 LINQ 的强大和灵活。
|using System; using System.Linq; using System.Collections.Generic; record Student(string Name, int Age, List<string> Tags); class Program { static void Main() { var students
SelectMany很重要:它把“每个学生的多个标签”拍扁成“所有标签的一个序列”。
想象一下,我们班上有一群同学,每个人都有自己的兴趣爱好。现在,我们要用 LINQ 来帮忙做一些有趣的“数据魔法”:
|using System; using System.Linq; using System.Collections.Generic; record Score(string Name, string Subject, int Points); class Program { static void Main() { var data = new List
GroupBy返回的是“分组序列”,每组有一个 Key 与一个元素序列。ToLookup则是“只读多值字典”,查询更方便。
|using System; using System.Linq; using System.Collections.Generic; record Student(int Id, string Name, int ClassId); record Class(int Id, string Title); class Program { static
我们在用 LINQ 处理“表和表之间的拼接”时,其实有两种常见的写法。第一种叫“方法语法”,这里我们会用到 Join 和 GroupJoin 这两个方法。
比如说,我们想把学生和班级的信息拼在一起,就像数据库里的“内连接”那样,这时候 Join 就派上用场了。
而如果我们想让每个班级都带上它的学生列表,那就用 GroupJoin,它能帮我们把一对多的关系整理得清清楚楚。
LINQ 还有一种“查询语法”,它的写法和 SQL 里的表连接特别像。比如 from ... join ... on ... equals ... select ... 这种结构,看起来就像在写数据库查询一样,非常直观。
如果我们以前接触过 SQL,看到这种写法会觉得特别亲切。当然不会SQL也没事,本站会有详细的SQL教程。
|using System; using System.Linq; int[] arr = {1,2,3}; Console.WriteLine(arr.Any()); // True Console.WriteLine(arr.All(x=>x>0)); // True Console.WriteLine(arr.Contains(
有时候我们用 LINQ 查找数据时,可能会遇到“空序列”这种情况。比如说,我们想找第一个符合条件的元素,通常会用 First() 或者 Single()。
但是,如果序列里根本没有符合条件的元素,这两个方法就会直接抛出异常,让程序崩溃。想象一下,我们在食堂排队打饭,结果窗口里啥都没有,这时候直接伸手去拿肯定会扑空,还可能摔一跤。
为了避免这种尴尬,LINQ 还贴心地提供了 FirstOrDefault() 和 SingleOrDefault()。
它们的意思就是:“如果找不到,就给我一个默认值(比如数字类型就是 0,引用类型就是 null),别直接报错。”
这样我们就能优雅地处理“啥都没有”的情况,不用担心程序突然罢工。
|using System; using System.Linq; using System.Collections.Generic; var actions = new List<Action>(); for (int i = 0; i < 3; i++) { actions.Add(()=>Console.WriteLine
在 LINQ 查询中,我们也经常会碰到闭包变量捕获带来的小陷阱。比如说,我们在写一个循环,然后在循环里用 lambda 表达式去捕获循环变量,这时候如果不小心,lambda 里用到的变量其实是同一个引用,等到真正执行的时候,变量的值可能早就变了,结果就和我们想象的不一样了。
在 LINQ 里,如果我们在循环里写 Where(x => x == i) 这样的代码,i 其实是被所有 lambda 共享的。等到 LINQ 查询真正执行时,i 的值已经变成了循环的最后一个值,所以所有的 lambda 都用的是同一个 i,结果就会让人很懵。
要解决这个问题,我们可以在循环体里新建一个临时变量,把当前的 i 赋值给它,然后让 lambda 捕获这个临时变量。这样,每个 lambda 都有自己独立的变量,就不会串台了。这个小技巧在写 LINQ 查询时非常实用,能帮我们避免很多意想不到的 bug。
有时我们想实现分页查询,比如从数据库里取出第 2 页,每页 10 条数据。这时候,我们通常需要先查出总数,再根据总数计算出需要跳过多少条数据。
|int pageIndex = 2, pageSize = 10; var page = await db.Orders .Where(o=>o.Total>=0) .OrderBy(o=>o.Id) .Skip((pageIndex-1)*pageSize) .Take(pageSize) .ToListAsync
分页通常分两次查询:一条查数据,一条查总数。把过滤条件保持一致。
在我们用 EF Core 操作数据库时,经常会遇到需要异步执行查询的场景。比如说,我们不希望因为等待数据库响应而让整个程序卡住,这时候就要用到异步方法。
EF Core 里,几乎所有常用的 LINQ 查询方法都有一个以 Async 结尾的异步版本,比如 ToListAsync、FirstOrDefaultAsync、AnyAsync 等等。
我们在调用这些异步方法时,通常会配合 await 关键字一起用,这样可以让程序在等待数据库结果的时候,先去忙别的事情,等数据查出来了再回来继续执行。
这样一来,程序的响应速度就会大大提升,尤其是在 Web 应用里,能让用户感觉到页面很流畅。
举个例子,假如我们要查找订单表里有没有总金额大于 1000 元的订单,我们可以这样写:
|bool hasBig = await db.Orders.AnyAsync(o=>o.Total>1000); var first = await db.Customers.FirstOrDefaultAsync(c=>c.Name=="小王");
LINQ 还提供了一些新增操作,比如 Concat、Union、Intersect、Except、Append、Prepend 等,这些方法可以帮助我们更方便地操作集合。
|using System; using System.Linq; int[] a = {1,2,3}; int[] b = {3,4,5}; Console.WriteLine(string.Join(",", a.Concat(b))); // 1,2,3,3,4,5
在我们用 Union、Intersect、Except 这些集合相关的 LINQ 方法时,系统会默认用元素自己的“相等性比较器”来判断两个元素是不是一样。
比如说,如果我们是用 int、string 这些常见类型,C# 已经帮我们内置好了判断方法。
但有时候,我们用的是自定义的类,比如订单、学生等,这时候如果我们想让 LINQ 按照我们自己的规则来判断“相等”,就可以自己写一个实现了 IEqualityComparer<T> 接口的比较器,然后把它传给这些方法。
有时候,我们手头有一大堆数据,比如一串数字、名字、订单等等,我们不想一个一个慢慢处理,而是希望能“分批”来搞定,或者把两组数据一一配对起来。 这个时候,LINQ 就像一个聪明的小帮手,给我们准备了很多顺手的工具,让这些批处理和配对的工作变得特别轻松。
比如说,假设我们有一串数字,想每三个分成一组,像打包快递一样打包好;又或者我们有两组数据,想把它们像拉红线一样一一配对,LINQ 都能帮我们一行代码搞定。
|using System; using System.Linq; int[] nums = {1,2,3,4,5,6,7}; foreach (var chunk in nums.Chunk(3)) Console.WriteLine(string.Join
Chunk 适合分页式处理;Zip 适合合并两个流形成配对数据。
4. Where + Select 顺序练习
分析以下代码,理解 Where 和 Select 的顺序对结果的影响:
Select(平方),再 Where(筛选偶数)Where 放前面,结果会怎样?|using System; using System.Linq; class Program { static void Main() { // 方式1:先 Select 再 Where(原代码) var q1 = Enumerable.Range(1, 6) .Select(x => x * x) // 先计算平方:1,4,9,16,25,36 .Where(
5. SelectMany 扁平化练习
使用 SelectMany 将嵌套数组扁平化:
new[]{ new[]{1,2}, new[]{3}, new[]{4,5,6} }SelectMany 将其扁平化为 1,2,3,4,5,6 的序列SelectMany 用于将"集合的集合"扁平化为单个集合Main 方法中测试并输出结果|using System; using System.Linq; class Program { static void Main() { var boxes = new[] { new[] { 1, 2 }, new[] { 3 }, new[] { 4, 5, 6 } }; // 使用 SelectMany 扁平化
6. GroupBy 平均分练习
使用 GroupBy 按姓名分组,并计算每个学生的平均分:
record S(string Name, string Subject, int Score);new[]{ new S("A","M",90), new S("A","C",80), new S("B","M",70) }GroupBy 分组,然后对每组使用 Average 计算平均值|using System; using System.Linq; record S(string Name, string Subject, int Score); class Program { static void Main() { var data = new[] { new S("A",
7. 连接两表练习
使用 LINQ 连接两个表,计算每个产品的总销量:
record P(int Id, string Name); 数据:new[]{ new P(1,"Pen"), new P(2,"Book") }record O(int Pid, int Qty); 数据:new[]{ new O(1,2), new O(1,3), new O(2,1) }Join 或 GroupJoin 连接,然后使用 Sum 计算总销量|using System; using System.Linq; record P(int Id, string Name); record O(int Pid, int Qty); class Program { static void Main() { var ps = new[] { new
8. 避免重复枚举练习
当需要多次使用 LINQ 查询结果时,如何避免重复计算:
GetExpensiveSequence() 返回一个昂贵的序列(比如需要查询数据库)ToList() 或 ToArray() 将结果缓存起来|using System; using System.Linq; class Program { static System.Collections.Generic.IEnumerable<int> GetExpensiveSequence() { Console.WriteLine("执行昂贵的操作..."); for (int i = 1; i <= 5; i
|方式1(先Select后Where): 4 16 36 方式2(先Where后Select): 4 16 36
说明:
Where 再 Select 更高效,因为减少了不必要的计算|扁平化结果: 1 2 3 4 5 6 数组长度: 6
说明:
SelectMany 用于将"集合的集合"扁平化为单个集合SelectMany(box => box) 表示:对每个内部数组,直接返回其元素[1,2], [3], [4,5,6] → 1,2,3,4,5,6SelectMany 常用于处理嵌套集合,比如每个学生有多个标签,需要把所有标签合并成一个列表|学生平均分: A: 85.0 B: 70.0
说明:
GroupBy(s => s.Name) 按姓名分组,返回 IGrouping<string, S>g.Key 是分组的键(学生姓名)g.Average(s => s.Score) 计算该组所有学生的平均分GroupBy 常用于数据统计和分组聚合操作|方式1(Join + GroupBy): Pen: 5 Book: 1 方式2(GroupJoin): Pen: 5 Book: 1
说明:
Join 连接两个表,然后用 GroupBy 分组,最后用 Sum 求和GroupJoin(分组连接),直接将订单分组到产品下,然后求和GroupJoin 更简洁,适合"一对多"关系的连接Join 和 GroupJoin 是 LINQ 中处理表连接的重要方法|=== 错误示例:重复枚举 === 执行昂贵的操作... 生成元素: 1 生成元素: 2 生成元素: 3 生成元素: 4 生成元素: 5 第一次遍历: 1 4 9 16 25 第二次遍历: 执行昂贵的操作... 生成元素: 1 生成元素: 2 生成元素: 3 生成元素: 4 生成元素: 5 1 4 9 16 25 === 正确示例:缓存结果 === 执行昂贵的操作... 生成元素: 1 生成元素: 2 生成元素: 3 生成元素: 4 生成元素: 5 第一次遍历: 1 4 9 16 25 第二次遍历: 1 4 9 16 25
说明:
ToList() 或 ToArray() 立即执行查询并缓存结果ToList() 返回 List<T>,ToArray() 返回数组,根据需求选择