INQ 是 语言集成查询 (Language Integrated Query) 的缩写,它允许你直接在 C# 里用声明式的语法来查询和转换各种数据源(如集合、数据库、XML),而不用写循环。
LINQ 的写法主要分两种,它们的核心是一样的,只是语法不同。
1. 两种语法风格
-
查询语法 (Query Syntax):很像 SQL,更接近自然语言,可读性好。
-
方法语法 (Method Syntax / Lambda 语法) :通过扩展方法和 Lambda 表达式 串联调用,功能更全,也更常用。
假设我们有一个学生列表,要从中找出成绩大于80分的学生名字:
csharp
// 准备数据
List<Student> students = new List<Student>()
{
new Student { Name = "张三", Score = 85 },
new Student { Name = "李四", Score = 70 },
new Student { Name = "王五", Score = 92 }
};
// 1. 查询语法 (Query Syntax)
var queryResult = from s in students
where s.Score > 80
orderby s.Name
select s.Name;
// 2. 方法语法 (Method Syntax / Lambda)
var methodResult = students
.Where(s => s.Score > 80) // 过滤
.OrderBy(s => s.Name) // 排序
.Select(s => s.Name); // 投影
这两种写法都会被编译器翻译成完全相同的 IL 代码,结果也完全一样
2. 核心操作符分三类
LINQ 的操作就像一条数据处理的流水线,理解它的关键在于"延迟执行"。
第一步:获取数据
万事俱备,先得有数据源。任何实现了 IEnumerable<T> 接口的对象都可以用 LINQ。
1. IEnumerable<T> 到底是什么?
它是一个极其简单的接口,里面只定义了一个核心功能:获取枚举器(GetEnumerator) 。 大白话来说:只要一个类实现了 IEnumerable<T>,就等于向 C# 编译器发誓:"我肚子里的数据,是排着队的一串序列,你可以用 foreach 循环把它们挨个拿出来看!"
2. 底层逻辑(为什么 LINQ 要挂在它身上?)
因为 LINQ 的本质就是"遍历数据 -> 制定规则 -> 提取结果"。 既然所有能被遍历的集合都实现了 IEnumerable<T>,微软的架构师就非常聪明地把 LINQ 所有的扩展方法(this IEnumerable<T> source)都挂载到了这个最基础的接口上。这就是为什么你的数组和 List 都能点(.)出 Where 和 OrderBy 的原因。
都有谁实现了 IEnumerable<T>?(家族大盘点)
除了你已经知道的 List<T>(动态列表)和 T[](数组),C# 中几乎所有用来"装东西"的集合都实现了它:
-
Dictionary<TKey, TValue>(字典):- 极度常用!它里面装的是键值对。当你用 LINQ 遍历字典时,你拿到的每一个元素是一个
KeyValuePair<TKey, TValue>对象。
- 极度常用!它里面装的是键值对。当你用 LINQ 遍历字典时,你拿到的每一个元素是一个
-
HashSet<T>(哈希集合):- 它的特点是绝对去重 且查找极快。如果你有一堆数据需要去重,放进这里面,它同样支持 LINQ。
-
Queue<T>(队列) /Stack<T>(栈):- 队列是"先进先出"(排队买票),栈是"后进先出"(叠盘子)。它们也能用 LINQ。
-
IQueryable<T>(数据库查询):- 这个极其关键!它是
IEnumerable<T>的"升级版子接口"。Entity Framework Core 操作数据库时用的就是它。它能把你的 LINQ 翻译成真实的 SQL 语句发给数据库。
- 这个极其关键!它是
-
String(字符串):- 没想到吧?字符串本质上是一个字符数组。所以
string其实实现了IEnumerable<char>。这意味着你可以直接对一个字符串用 LINQ,比如:"Hello".Where(c => c == 'l')。
- 没想到吧?字符串本质上是一个字符数组。所以
易混淆知识点打擂台(集合的阶级森严)
很多新手分不清 IEnumerable<T>、ICollection<T> 和 IList<T>。其实它们是"爷爷、爸爸、儿子"的继承关系,功能越来越强:
| 接口级别 | 核心能力 | 包含的方法/属性 | 大白话比喻 |
|---|---|---|---|
爷爷:IEnumerable<T> |
只能看(遍历) | GetEnumerator() |
博物馆的玻璃展柜,你只能挨个看,不能摸。 |
爸爸:ICollection<T> |
能看,且能增删、能数个数 | 多了 .Count, .Add(), .Remove() |
你自己的储物箱,你可以往里扔东西,也能拿出来,也能数数。 |
儿子:IList<T> |
能看、能增删,且能按位置精准插拔 | 多了索引器 [index], .Insert() |
带有编号的中药柜,你可以精准指着第 3 个抽屉说:"把里面的东西换掉"。 |
架构设计边界指南:
-
✅ 适用场景: 当你写一个函数,只需要读取 数据时,参数类型尽量写
IEnumerable<T>。这叫"宽进",无论别人传数组还是 List 给你,你的函数都不用改。 -
🚫 禁止场景: 如果你的函数内部需要往集合里
Add()添加元素,绝对禁止 将参数写成IEnumerable<T>(因为它没有 Add 方法),此时应该写ICollection<T>或IList<T>。
第二步:数据转换 (最重要)
这条流水线的核心是惰性求值 。像 Where、Select、OrderBy 这些方法,你调用时它不会立即执行 ,只是搭了个查询管道。只有到你真正去用数据(比如 foreach 遍历或调用 ToList())时,它才会从头到尾把数据处理一遍。
-
过滤
Where:Where(s => s.Score > 80),筛出满足条件的。 -
投影
Select:Select(s => new { s.Name, s.Score }),把元素转换成想要的形状,还支持匿名类型。 -
排序
OrderBy/ThenBy:OrderBy(s => s.Score).ThenBy(s => s.Name),先按分数再按姓名排序。降序是OrderByDescending。 -
分组
GroupBy:GroupBy(s => s.Grade),按年级分组,会把数据变成"钥匙-一组值"的结构。 -
连接
Join:students.Join(classes, s => s.ClassId, c => c.Id, (s, c) => ...),和 SQL 的连表一样,关联两个集合。 -
去重
Distinct:Distinct(),去除重复项。
第三步:立即执行求值
这些方法会立刻触发查询"流水线",得到最终结果。
-
转为集合 :
.ToList()、.ToArray()、.ToDictionary() -
聚合计算 :
.Count()、.Sum()、.Average()、.Max()、.Min() -
取单个元素 :
.First()(无则抛异常)、.FirstOrDefault()(无则返回默认值 null/0)、.Single()(确保只有一个)。OrDefault系列非常常用。
五、实战对象举例(最能理解)Linq语句方法后都是什么λ类型表达式
假如你有一个学生类:
class Student
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
List<Student> list = ...;
1. Where(判断)
list.Where(s => s.Age > 18);
// Func<Student, bool> 输入学生 → 输出 bool
2. OrderBy(提取排序键)
list.OrderBy(s => s.Age);
// Func<Student, int> 输入学生 → 输出 int(年龄)
3. ThenBy(提取次要排序键)
list.OrderBy(s => s.Class).ThenBy(s => s.Score);
// 都是 Func<Student, 键类型>
4.Select 里的 Lambda 类型是:
Func<TSource, TResult>
六、最终超级总结(必背)
- Where :Lambda = 条件 →
Func<T, bool> - OrderBy / ThenBy :Lambda = 排序键 →
Func<T, TKey>
你现在已经彻底吃透 LINQ + 泛型 + Lambda 底层原理了!
七、LINQ 四大金刚 Lambda 类型总结(超级完整版)
这张表背下来,LINQ 彻底毕业!
表格
| 方法 | Lambda 类型 | 作用 | 输入 → 输出 |
|---|---|---|---|
| Where | Func<T, bool> |
筛选 | 元素 → true/false |
| OrderBy/ThenBy | Func<T, TKey> |
排序 | 元素 → 排序键 |
| Select | Func<T, TResult> |
投影 / 转换 | 元素 → 任意新结果 |
| First/Any/Count | Func<T, bool> |
判断 | 元素 → true/false |