当前位置: 首页 > news >正文

C# LINQ 从入门到精通——用 SQL 一样的语法操作集合,Where、Select、GroupBy 带你告别 foreach 地狱

目录

  1. 为什么需要 LINQ?

  2. LINQ 的两种写法:查询语法与方法语法

  3. 最常用的 10 个标准查询运算符

  4. 延迟执行 vs 立即执行(重要概念)

  5. LINQ to Objects 实战:学生成绩分析

  6. LINQ to SQL 与 Entity Framework 初探

  7. 综合实战:员工薪资报表系统

  8. 练习题答案及解析

  9. 性能提示与避坑指南

  10. 下一步学习建议


1. 为什么需要 LINQ?

回想一下,没有 LINQ 之前,我们要从一个集合中筛选数据,通常要写繁琐的foreach循环 +if判断:

csharp

List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; List<int> evenNumbers = new List<int>(); foreach (var n in numbers) { if (n % 2 == 0) evenNumbers.Add(n); }

这还只是最简单的筛选,如果需要排序、分组、连接多个集合,代码会变得又长又难维护。

LINQ(Language Integrated Query,语言集成查询)就是为了解决这个问题而生的。它允许你用类似 SQL 的语法直接写在 C# 代码中,对数组、集合、数据库、XML 等数据源进行统一的查询操作。

核心优势

  • 可读性强:查询意图一目了然

  • 类型安全:编译时检查,避免运行时错误

  • 统一 API:无论数据源是什么,查询语法都一样


2. LINQ 的两种写法

LINQ 提供了两种等价的写法,你可以根据自己的喜好选择。

2.1 查询语法(Query Syntax)

看起来就像 SQL 语句,使用fromwhereselect等关键字:

csharp

int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; // 查询语法:筛选出偶数并按降序排列 var result = from n in numbers where n % 2 == 0 orderby n descending select n; foreach (var item in result) Console.Write(item + " "); // 输出:8 6 4 2

2.2 方法语法(Method Syntax / Fluent Syntax)

使用扩展方法(如WhereSelect)配合 Lambda 表达式:

csharp

var result = numbers.Where(n => n % 2 == 0) .OrderByDescending(n => n); // 输出同上

2.3 哪种写法更好?

  • 查询语法更接近 SQL,容易上手,但功能有限(不支持所有操作符,如TakeWhile)。

  • 方法语法更强大、更灵活,几乎所有 LINQ 操作都能用。

  • 推荐混用:复杂查询用查询语法,特殊操作用方法语法。大多数 C# 开发者偏好方法语法。


3. 最常用的 10 个标准查询运算符

运算符作用示例(方法语法)
Where条件筛选list.Where(x => x > 10)
Select投影(转换)list.Select(x => x * 2)
SelectMany展平嵌套集合list.SelectMany(x => x.Items)
OrderBy/OrderByDescending升序/降序排序list.OrderBy(x => x.Age)
ThenBy/ThenByDescending次要排序list.OrderBy(x => x.Age).ThenBy(x => x.Name)
GroupBy分组list.GroupBy(x => x.Category)
Join内连接list1.Join(list2, ...)
GroupJoin分组连接(左外连接)list1.GroupJoin(list2, ...)
Any/All是否存在/是否全部满足list.Any(x => x > 5)
First/FirstOrDefault取第一个元素list.First(x => x > 5)
Single/SingleOrDefault取唯一元素list.Single(x => x == 5)
Skip/Take分页list.Skip(10).Take(20)
Aggregate累计(求和、拼接等)list.Aggregate((a,b) => a + b)

每个运算符都返回一个新的集合或单个值,不会修改原始集合。


4. 延迟执行 vs 立即执行(重要概念)

这是新手最容易踩的坑!

4.1 延迟执行(Deferred Execution)

WhereSelectOrderBy这些返回IEnumerable<T>的操作符,它们不会立即执行,而是在你开始遍历结果时才真正去查询数据源。

csharp

var numbers = new List<int> { 1, 2, 3 }; var query = numbers.Where(n => n > 1); // 此时还没执行查询 numbers.Add(4); // 修改了数据源 foreach (var n in query) // 这里才执行查询,会包含 4 { Console.WriteLine(n); // 输出 2, 3, 4 }

好处:每次遍历都获取最新数据,且可以组合多个操作,只遍历一次集合。

4.2 立即执行(Immediate Execution)

ToList()ToArray()Count()First()等操作,会立即执行查询并返回结果(集合或单个值)。

csharp

var numbers = new List<int> { 1, 2, 3 }; var listResult = numbers.Where(n => n > 1).ToList(); // 立即执行 numbers.Add(4); foreach (var n in listResult) // 不会包含 4,因为已经固化 { Console.WriteLine(n); // 输出 2, 3 }

记住

  • 延迟执行的查询结果是一个“查询对象”,每次遍历都会重新执行。

  • 立即执行的结果是快照,不受后续数据源变化影响。


5. LINQ to Objects 实战:学生成绩分析

我们用一个学生列表来演示各种 LINQ 操作。

5.1 准备数据

csharp

public class Student { public int Id { get; set; } public string Name { get; set; } public string Class { get; set; } // 班级 public int Score { get; set; } } List<Student> students = new List<Student> { new Student { Id = 1, Name = "张三", Class = "A班", Score = 85 }, new Student { Id = 2, Name = "李四", Class = "B班", Score = 92 }, new Student { Id = 3, Name = "王五", Class = "A班", Score = 78 }, new Student { Id = 4, Name = "赵六", Class = "B班", Score = 88 }, new Student { Id = 5, Name = "孙七", Class = "A班", Score = 95 }, new Student { Id = 6, Name = "周八", Class = "C班", Score = 60 }, };

5.2 常见查询示例

csharp

// 1. 成绩大于 80 的学生(按分数降序) var highScorers = students.Where(s => s.Score > 80) .OrderByDescending(s => s.Score); foreach (var s in highScorers) Console.WriteLine($"{s.Name} - {s.Score}"); // 2. 只显示姓名和班级(投影) var nameClass = students.Select(s => new { s.Name, s.Class }); foreach (var item in nameClass) Console.WriteLine($"{item.Name} 在 {item.Class}"); // 3. 按班级分组,计算每个班的平均分 var groupByClass = students.GroupBy(s => s.Class) .Select(g => new { Class = g.Key, AvgScore = g.Average(s => s.Score), Count = g.Count() }); foreach (var g in groupByClass) Console.WriteLine($"班级 {g.Class}:平均分 {g.AvgScore:F2},人数 {g.Count}"); // 4. 最高分的学生(使用 OrderByDescending + First) var topStudent = students.OrderByDescending(s => s.Score).First(); Console.WriteLine($"最高分:{topStudent.Name} - {topStudent.Score}"); // 5. 是否存在不及格学生(Any) bool hasFailed = students.Any(s => s.Score < 60); Console.WriteLine($"存在不及格?{hasFailed}"); // 6. 所有学生是否都及格(All) bool allPassed = students.All(s => s.Score >= 60); Console.WriteLine($"全部及格?{allPassed}"); // 7. 取前 3 名(分页) var top3 = students.OrderByDescending(s => s.Score).Take(3); foreach (var s in top3) Console.WriteLine($"前三名:{s.Name} - {s.Score}"); // 8. 跳过前 3 名,取剩下的(分页) var rest = students.OrderByDescending(s => s.Score).Skip(3); foreach (var s in rest) Console.WriteLine($"其余:{s.Name} - {s.Score}");

6. LINQ to SQL 与 Entity Framework 初探

LINQ 不仅能查内存中的集合,还能查数据库!这是通过LINQ to SQLEntity Framework Core实现的。

当你使用 EF Core 时,可以这样查询数据库:

csharp

using (var db = new SchoolContext()) { // 查询成绩大于 80 的学生(实际会生成 SQL 语句) var highScorers = db.Students .Where(s => s.Score > 80) .OrderByDescending(s => s.Score) .ToList(); }

EF Core 会把你的 LINQ 表达式翻译成 SQL,发送给数据库执行,并返回对象。这就是LINQ to Entities

核心要点

  • 对于IQueryable<T>(EF Core 查询),LINQ 操作是延迟执行的,直到ToList()等立即执行方法才会生成 SQL。

  • 你写的 LINQ 语句不会加载所有数据到内存,而是生成高效的 SQL。


7. 综合实战:员工薪资报表系统

我们将构建一个控制台程序,使用 LINQ 对员工数据进行复杂的统计分析。

7.1 数据模型

csharp

public class Employee { public int Id { get; set; } public string Name { get; set; } public string Department { get; set; } public double Salary { get; set; } public DateTime HireDate { get; set; } }

7.2 生成测试数据

csharp

List<Employee> employees = new List<Employee> { new Employee { Id = 1, Name = "Alice", Department = "HR", Salary = 5000, HireDate = new DateTime(2020, 1, 15) }, new Employee { Id = 2, Name = "Bob", Department = "IT", Salary = 8000, HireDate = new DateTime(2019, 5, 20) }, new Employee { Id = 3, Name = "Charlie", Department = "IT", Salary = 9500, HireDate = new DateTime(2018, 3, 10) }, new Employee { Id = 4, Name = "David", Department = "Sales", Salary = 6000, HireDate = new DateTime(2021, 7, 1) }, new Employee { Id = 5, Name = "Eva", Department = "HR", Salary = 5200, HireDate = new DateTime(2020, 11, 12) }, new Employee { Id = 6, Name = "Frank", Department = "IT", Salary = 7000, HireDate = new DateTime(2022, 2, 28) }, new Employee { Id = 7, Name = "Grace", Department = "Sales", Salary = 7500, HireDate = new DateTime(2019, 9, 5) }, };

7.3 完整报表功能

csharp

static void Main() { // 1. 各部门平均薪资(降序) var deptAvg = employees.GroupBy(e => e.Department) .Select(g => new { Dept = g.Key, AvgSalary = g.Average(e => e.Salary) }) .OrderByDescending(x => x.AvgSalary); Console.WriteLine("=== 各部门平均薪资 ==="); foreach (var item in deptAvg) Console.WriteLine($"{item.Dept}: {item.AvgSalary:C}"); // 2. 薪资最高的前 3 名员工 var top3 = employees.OrderByDescending(e => e.Salary) .Take(3) .Select(e => new { e.Name, e.Salary, e.Department }); Console.WriteLine("\n=== 薪资 Top 3 ==="); foreach (var e in top3) Console.WriteLine($"{e.Name} ({e.Department}) - {e.Salary:C}"); // 3. 各部门入职人数(按年份分组) var hireByDeptYear = employees.GroupBy(e => e.Department) .Select(g => new { Dept = g.Key, Data = g.GroupBy(e => e.HireDate.Year) .Select(y => new { Year = y.Key, Count = y.Count() }) }); Console.WriteLine("\n=== 各部门年度入职人数 ==="); foreach (var dept in hireByDeptYear) { Console.WriteLine($"【{dept.Dept}】"); foreach (var yearData in dept.Data.OrderBy(y => y.Year)) Console.WriteLine($" {yearData.Year}年: {yearData.Count}人"); } // 4. 统计各部门薪资总额、最高、最低 var stats = employees.GroupBy(e => e.Department) .Select(g => new { Dept = g.Key, Total = g.Sum(e => e.Salary), Max = g.Max(e => e.Salary), Min = g.Min(e => e.Salary), Count = g.Count() }); Console.WriteLine("\n=== 各部门薪资统计 ==="); foreach (var s in stats) Console.WriteLine($"{s.Dept}: 总额 {s.Total:C}, 最高 {s.Max:C}, 最低 {s.Min:C}, 人数 {s.Count}"); // 5. 找出公司内薪资高于部门平均薪资的员工(关联子查询) var aboveDeptAvg = employees.Select(e => new { Employee = e, DeptAvg = employees.Where(emp => emp.Department == e.Department) .Average(emp => emp.Salary) }) .Where(x => x.Employee.Salary > x.DeptAvg) .Select(x => new { x.Employee.Name, x.Employee.Salary, DeptAvg = x.DeptAvg }); Console.WriteLine("\n=== 高于部门平均薪资的员工 ==="); foreach (var item in aboveDeptAvg) Console.WriteLine($"{item.Name} (薪资 {item.Salary:C}) 高于部门平均 {item.DeptAvg:C}"); }

7.4 运行效果(部分输出)

text

=== 各部门平均薪资 === IT: $8,166.67 Sales: $6,750.00 HR: $5,100.00 === 薪资 Top 3 === Charlie (IT) - $9,500.00 Bob (IT) - $8,000.00 Grace (Sales) - $7,500.00 ...

8. 练习题

练习 1:基本筛选与投影

给定一个整数列表List<int> numbers = new List<int> { 5, 12, 8, 3, 20, 15, 7, 9 };

  1. 用 LINQ 筛选出大于 10 的数字,并按降序输出。

  2. 将每个数字乘以 2,然后输出。

  3. 取出前 3 个偶数。

练习 2:分组与聚合(订单系统)

定义一个Order类(OrderId,CustomerId,Product,Quantity,Price)。创建包含 6~8 个订单的列表。

  1. CustomerId分组,计算每个客户的订单总金额。

  2. Product分组,计算每种产品的总销量和总销售额。

  3. 找出消费总额最高的客户 ID。

练习 3:复杂查询——员工与部门关联

定义Department类(DeptId,DeptName)和Employee类(EmpId,Name,DeptId,Salary)。
创建部门和员工的对应关系(多个员工属于一个部门)。

  1. 使用Join查询每个员工的姓名、薪资和部门名称。

  2. 按部门分组,计算每个部门的平均薪资,并输出部门名称和平均薪资。

  3. (选做)查询平均薪资高于公司平均薪资的部门。


9. 练习题答案及解析

练习1 答案

csharp

// 1. 大于10降序 var result1 = numbers.Where(n => n > 10).OrderByDescending(n => n); // 2. 每个乘2 var result2 = numbers.Select(n => n * 2); // 3. 前3个偶数 var result3 = numbers.Where(n => n % 2 == 0).Take(3);

练习2 答案(核心代码)

csharp

// 按客户分组,计算总金额 var customerTotal = orders.GroupBy(o => o.CustomerId) .Select(g => new { CustomerId = g.Key, Total = g.Sum(o => o.Quantity * o.Price) }); // 按产品分组 var productStats = orders.GroupBy(o => o.Product) .Select(g => new { Product = g.Key, TotalQuantity = g.Sum(o => o.Quantity), TotalSales = g.Sum(o => o.Quantity * o.Price) }); // 最高消费客户 var topCustomer = customerTotal.OrderByDescending(x => x.Total).First().CustomerId;

练习3 答案(Join 示例)

csharp

// 1. Join 查询 var empWithDept = employees.Join(departments, e => e.DeptId, d => d.DeptId, (e, d) => new { e.Name, e.Salary, DeptName = d.DeptName }); // 2. 按部门平均薪资 var deptAvg = employees.GroupBy(e => e.DeptId) .Join(departments, g => g.Key, d => d.DeptId, (g, d) => new { DeptName = d.DeptName, AvgSalary = g.Average(e => e.Salary) }); // 3. 高于公司平均的部门(选做) double companyAvg = employees.Average(e => e.Salary); var aboveAvgDepts = deptAvg.Where(x => x.AvgSalary > companyAvg);

10. 性能提示与避坑指南

⚠️ 陷阱1:多次遍历同一个延迟查询

csharp

var query = numbers.Where(n => n > 5); int count = query.Count(); // 第一次遍历 foreach (var n in query) { } // 第二次遍历

如果数据源是数据库查询,这会导致两次 SQL 执行。解决:如果反复使用,先ToList()缓存。

⚠️ 陷阱2:在 LINQ 中修改数据源

foreach遍历查询结果时修改原始集合会引发异常。应先ToList()再遍历。

⚠️ 陷阱3:滥用Count()判断是否为空

csharp

if (list.Count() > 0) { } // ❌ 会遍历整个集合 if (list.Any()) { } // ✅ 只要发现一个元素就返回 true,性能更好

⚠️ 陷阱4:在Where中调用耗时方法

Where中的委托会在每次遍历时执行,如果里面有耗时操作(如网络请求),性能会很差。解决:先在内存中准备好数据,或使用Select预处理。

✅ 最佳实践

  • 优先使用Any()而非Count() > 0

  • 链式调用时,把Where等筛选操作放在前面,减少后续处理的数据量。

  • 对数据库查询,避免在Where中使用不能翻译为 SQL 的本地方法(如DateTime.Now的复杂计算)。

  • 使用Select投影时,只选择需要的字段,减少数据传输量。


11. 下一步学习建议

  • 表达式树(Expression Trees):了解 LINQ to SQL 背后的编译原理。

  • PLINQ(并行 LINQ):利用多核处理器加速查询。

  • 动态 LINQ:在运行时构建查询字符串(可用于动态报表)。

  • 深入 EF Core:掌握导航属性、Include、过滤等高级查询。

  • 实战项目:用 LINQ 重构之前的学生管理系统、游戏角色系统,把循环全部替换成 LINQ 语句。


最后的话

LINQ 是 C# 开发者最爱的特性之一,它让数据查询变得像呼吸一样自然。你可能一开始觉得查询语法和方法语法有点绕,但只要坚持写几个实际场景,就会爱上它。

今天的员工报表系统演示了 LINQ 在真实业务中的威力——分组、聚合、排序、关联查询一气呵成,代码清晰且易于维护。

记住两条黄金法则:

  1. 延迟执行是双刃剑——善用它,但也别忘了ToList()来固定结果。

  2. 能 LINQ 的就别手写循环——除非有极端的性能要求,否则 LINQ 的可读性和生产力远超手写。

把上面的练习做一遍,再试着用 LINQ 重写你以前写过的最复杂的查询逻辑,你会感受到质的飞跃。

下一篇我们将进入异常处理与调试的世界,让你的程序在出错时也能优雅应对。我们下一篇见!🚀


下一篇预告:C# 异常处理与调试技巧——try-catch-finally全方位解读,教你写健壮的程序,并掌握 Visual Studio 调试利器。敬请期待!

(全文完)

http://www.jsqmd.com/news/1214354/

相关文章:

  • 秀英区伯爵金饰回收实测,连锁门店盘点,逸程全海口四区可预约上门 - 全城热点
  • 2026上海杨浦五角场黄金回收避坑经验,本地连锁大品牌无套路报价,免费上门收金当场转账 - 一日一测评
  • Java实现列车时刻表数据处理:从时间格式化到业务逻辑完整实战
  • 八字排盘命理软件推荐:2026最新先看退出与迁移成本
  • AutoMQ x 阿里云 OSS Tables:基于 Iceberg 构建流表一体的实时入湖
  • 2026年UV油墨丨UV水晶标签打印机丨UV 打印机丨UV DTF水晶标签打印机厂家排名 高口碑实力品牌行业参考指南 - 变量人生001
  • 济南哪家民办中专学校比较不错
  • 跨国多云架构设计:如何优雅搞定 AWS / 阿里云 / 腾讯云国际站的多云治理与 FinOps 实践
  • 强力磁铁高吸力耐用 工业通用强磁 源头厂家定制 - 优企甄选
  • 2026西安闲置大牌包包出售怎么避坑?易奢福正规回收全方位解析 - 易奢福
  • AI Agent自动化代码安全扫描与智能修复实践指南
  • 北海黄金贵金属回收指南:六家靠谱店铺全区域覆盖推荐 - 清奢黄金上门回收
  • 从轨迹复现到通用智能:解析特斯拉Optimus与宇树机器人的技术路线差异
  • 2026东营屋顶漏水维修怎么做?对比3家正规公司报价与施工方案(7月实测) - 吉林同城获客
  • 2026阿拉善盟别墅自建施工怎么选?TOP 砖混 + 框架 + 轻钢 + 庭院一体化 附电话地址 - 中检检测集团
  • 2026济南屋顶漏水维修怎么做?对比3家正规公司报价与施工方案(7月实测) - 吉林同城获客
  • 植物提取物定制全流程拆解:从原料筛样到吨级量产的四大标准阶段
  • 市面上满足各类环境测试需求内存颗粒NAND芯片测试座厂家适配性强
  • 探寻UV油墨丨UV水晶标签打印机丨UV 打印机丨UV DTF水晶标签打印机公司哪个好?2026年UV DTF打印机品牌优选指南 - 变量人生001
  • 终极免费开源音乐播放器:洛雪音乐助手桌面版完整指南
  • 25GB内存跑744B大模型:Colibri如何用单文件C引擎颠覆AI推理的硬件门槛
  • 如何用Onekey免费解锁Steam游戏:5分钟完整指南
  • 前端 ttf / woff2 字体资源完整优化方案
  • 欧米茄珠海售后维修服务中心|珠海欧米茄售后维修服务网点位置 + 统一客服 400-883-8097 权威信息公告(2026 年 7 月最新) - 欧米茄中国售后中心
  • 济南黄金回收探店实测:头部商家金价领先同行,当面称重当场转账 - 奢侈品回收评测
  • 2026.7.18 考试总结
  • 2026快手视频去水印方法:合法合规教程,官方无水印保存与工具风险解析
  • 研0怎样才能高效读文献?
  • 微信公众号2026年迁移公证是什么?线上办理方法详解 - 跑政通
  • (九)在MCU上跑AI推理:ESP-DL vs TFLite Micro,选哪个?怎么优化?