LINQ
C# LINQ
一、Lambda 表达式:从匿名方法到语法糖
1. 委托与匿名方法
Lambda 表达式本质是简化写法的匿名方法,最终都会被编译为委托实例。在 Lambda 语法出现之前,使用 delegate 关键字定义匿名方法:
// 匿名方法写法
Func<int, int, int> f1 = delegate (int i, int j)
{
return i + j;
};
Console.WriteLine(f1(3, 5)); // 输出 8
Func<in T1, in T2, out TResult>:有返回值的泛型委托,最后一个泛型参数为返回值类型。- 对应的无返回值泛型委托为
Action<T1, T2>。
2. Lambda 表达式语法
Lambda 使用 => 运算符连接参数列表与方法体,编译器可根据委托类型自动推断参数类型。
完整写法:
Func<int, int, int> f2 = (int i, int j) =>
{
return i + j;
};
Console.WriteLine(f2(3, 4)); // 输出 7
简化规则:
- 参数类型可省略,由编译器推断:
(i, j) => { return i + j; } - 方法体只有一行代码且有返回值时,可省略
{}和return:
Func<int, int, string> f3 = (i1, i2) => $"{i1}+{i2}={i1 + i2}";
Console.WriteLine(f3(1, 2)); // 输出 1+2=3
- 单个参数时,参数外的括号可省略:
Func<int, bool> isEven = x => x % 2 == 0;
二、LINQ 概述与底层原理
1. 什么是 LINQ
LINQ(Language Integrated Query,语言集成查询)是 C# 中用于统一处理各类集合数据的语法体系,核心是为 IEnumerable<T> 提供了一系列扩展方法,配合 Lambda 表达式可以简洁地完成数据筛选、排序、投影、聚合等操作。
- 核心命名空间:
using System.Linq; - 支持数据源:数组、
List<T>、EF Core 数据库、XML 等所有实现IEnumerable<T>的类型
2. LINQ 方法的本质:扩展方法 + 迭代器
LINQ 的筛选、投影等方法本质是扩展方法,内部通过迭代器模式实现。我们可以手写一个简易版 Where 来理解其原理:
版本1:立即执行(一次性生成所有结果)
static IEnumerable<int> MyWhere1(IEnumerable<int> items, Func<int, bool> f)
{
List<int> result = new List<int>();
foreach (int i in items)
{
if (f(i))
result.Add(i);
}
return result;
}
特点:调用时立即执行所有循环,将结果全部存入 List 后返回,属于立即执行。
版本2:延迟执行(yield return 迭代器)
static IEnumerable<int> MyWhere2(IEnumerable<int> items, Func<int, bool> f)
{
foreach (int i in items)
{
if (f(i))
yield return i;
}
}
特点:使用 yield return 构建迭代器,调用方法时不会立即执行,只有在遍历(foreach、ToList、Count 等)时才会逐个生成元素,即延迟执行(惰性求值)。
系统内置的 LINQ 方法(Where、Select、OrderBy 等)几乎都采用延迟执行机制,这是 LINQ 高效的核心原因之一。
3. var 与类型推断
LINQ 中常用 var 简化类型声明,由编译器自动推断变量类型:
int[] nums = { 3, 99, 88, 77, 7, 8, 9, 66, 15, 7 };
// 等同于 IEnumerable<int> items = nums.Where(i => i > 10);
var items = nums.Where(i => i > 10);
注意:C# 的 var 是强类型,编译时就已确定类型,和 JavaScript 的弱类型 var 完全不同。C# 中的弱类型是 dynamic。
三、LINQ 常用扩展方法详解
以下示例统一使用测试数据:
List<Student> students = new List<Student>
{
new Student { Id = 1, Name = "张三", Age = 18, Score = 95, ClassId = 1 },
new Student { Id = 2, Name = "李四", Age = 19, Score = 82, ClassId = 1 },
new Student { Id = 3, Name = "王五", Age = 17, Score = 60, ClassId = 2 },
new Student { Id = 4, Name = "赵六", Age = 20, Score = 78, ClassId = 2 },
new Student { Id = 5, Name = "孙七", Age = 18, Score = 90, ClassId = 1 },
};
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public int Score { get; set; }
public int ClassId { get; set; }
}
1. 筛选:Where
按条件筛选集合元素,返回满足条件的元素集合。
// 筛选分数大于80的学生
var highScoreStudents = students.Where(s => s.Score > 80);
2. 数量判断:Count 与 Any
- Count():返回满足条件的元素总数
int count = students.Count(s => s.Age >= 18); // 结果:4
- Any():判断是否至少存在一条满足条件的数据,返回 bool
bool hasFail = students.Any(s => s.Score < 60); // 结果:false
性能提示:只需要判断“有没有”时,优先用
Any(),比Count() > 0效率更高,因为找到第一条匹配项就会停止遍历。
3. 获取单个元素
| 方法 | 适用场景 | 异常情况 |
|---|---|---|
Single() |
有且只有一条满足条件的数据 | 0条或多条都会抛出异常 |
SingleOrDefault() |
最多一条满足条件的数据 | 多条时抛异常;0条返回默认值 |
First() |
至少有一条满足条件,取第一条 | 0条时抛异常 |
FirstOrDefault() |
取第一条,没有则返回默认值 | 不会因数量不足抛异常 |
代码示例:
// 有且只有一个Id=1的学生,正常返回
var s1 = students.Single(s => s.Id == 1);
// 不存在Id=10的学生,抛出异常
// var s2 = students.Single(s => s.Id == 10);
// 不存在则返回 null
var s3 = students.SingleOrDefault(s => s.Id == 10);
// 取班级1的第一个学生
var first = students.First(s => s.ClassId == 1);
4. 排序
- OrderBy():按指定字段升序排序
- OrderByDescending():按指定字段降序排序
- ThenBy() / ThenByDescending():多规则次级排序,必须跟在 OrderBy 之后
// 先按班级升序,同班再按分数降序
var sorted = students
.OrderBy(s => s.ClassId)
.ThenByDescending(s => s.Score);
5. 分页:Skip 与 Take
- Skip(n):跳过前 n 条数据
- Take(n):只取前 n 条数据
组合实现分页(第2页,每页2条):
var page2 = students
.OrderBy(s => s.Id)
.Skip(2) // 跳过前2条
.Take(2); // 取2条
6. 聚合函数
对集合进行数值统计计算:
int maxScore = students.Max(s => s.Score); // 最高分 95
int minScore = students.Min(s => s.Score); // 最低分 60
double avgScore = students.Average(s => s.Score); // 平均分 81
int totalScore = students.Sum(s => s.Score); // 总分 405
int count = students.Count(); // 总人数 5
7. 分组:GroupBy
按指定字段对集合分组,返回 IGrouping<TKey, TElement> 集合,每个分组包含 Key 和组内元素。
// 按班级分组
var classGroups = students.GroupBy(s => s.ClassId);
foreach (var group in classGroups)
{
Console.WriteLine($"班级 {group.Key}:");
foreach (var s in group)
{
Console.WriteLine($" {s.Name} - {s.Score}分");
}
}
8. 投影:Select 与匿名类型
投影就是把集合中的每一项转换成另一种类型,常用 Select 实现。
基础投影
// 只提取学生姓名集合
IEnumerable<string> names = students.Select(s => s.Name);
匿名类型投影
可以使用 new {} 生成匿名类型,自定义字段名和内容,无需提前定义类:
var studentInfo = students.Select(s => new
{
XingMing = s.Name,
NianLing = s.Age,
ScoreLevel = s.Score >= 80 ? "优秀" : "及格"
});
foreach (var item in studentInfo)
{
Console.WriteLine($"{item.XingMing} - {item.NianLing}岁 - {item.ScoreLevel}");
}
9. 链式调用
LINQ 方法大多返回 IEnumerable<T>,因此可以链式拼接多个操作,形成完整的数据处理流水线:
var result = students
.Where(s => s.Score > 60) // 筛选及格的
.GroupBy(s => s.ClassId) // 按班级分组
.OrderBy(g => g.Key) // 按班级号排序
.Take(2) // 取前2个班
.Select(g => new // 投影统计结果
{
BanJi = g.Key,
RenShu = g.Count(),
PingJunFen = g.Average(s => s.Score)
});
四、LINQ 的两种语法:方法语法 vs 查询语法
LINQ 有两种书写风格,最终编译结果完全一致。
1. 方法语法(Method Syntax)
上面所有示例都属于方法语法,通过扩展方法 + Lambda 调用,是最灵活、最常用的写法。
2. 查询语法(Query Syntax)
类似 SQL 的书写风格,使用 from、where、orderby、select 等关键字,编译器会自动转换为方法语法。
var query = from s in students
where s.Score > 80
orderby s.Score descending
select new
{
s.Name,
s.Score
};
- 适用场景:复杂的多表连接、分组查询时,查询语法可读性更好。
- 限制:并非所有 LINQ 方法都有对应的查询语法关键字(如 Skip、Take、Any 等),复杂场景仍需混合方法语法。
五、LINQ 核心特性:延迟执行
1. 什么是延迟执行
像 Where、Select、OrderBy、GroupBy 这类返回 IEnumerable<T> 的方法,调用时不会立即执行查询逻辑,只是构建了查询计划。只有当数据被真正使用时(遍历、转为集合、聚合计算),查询才会执行。
int[] nums = { 1, 2, 3, 4 };
var query = nums.Where(x => x > 2); // 此时并未执行筛选
nums[2] = 100; // 修改原数组
foreach (var n in query) // 遍历的时候才真正执行筛选
{
Console.WriteLine(n); // 输出 100, 4
}
结果输出 100 和 4,证明查询是在遍历的时候才执行的,会读取最新的数据源。
2. 立即执行的方法
以下操作会触发查询立即执行:
- 转为集合:
ToList()、ToArray()、ToDictionary() - 聚合计算:
Count()、Sum()、Max()、First()、Any() - 遍历:
foreach循环
3. 注意事项
- 重复枚举会重复执行:每次 foreach 都会重新执行一次查询,数据源变化时结果可能不同。
- 复杂查询可缓存:如果需要多次使用结果,建议调用
ToList()立即执行并缓存结果,避免重复计算。
六、补充:更多常用 LINQ 方法
1. All
判断集合中所有元素是否都满足条件:
bool allPass = students.All(s => s.Score >= 60); // true
2. Distinct
去除集合中的重复元素:
int[] nums = { 1, 2, 2, 3, 3, 3 };
var distinctNums = nums.Distinct(); // 1, 2, 3
3. SelectMany
将集合的每个子集合“扁平化”成一个单一集合:
// 例如每个班级有多个学生,把所有班级的学生合并成一个列表
List<Class> classes = ...;
var allStudents = classes.SelectMany(c => c.Students);
4. TakeWhile / SkipWhile
TakeWhile:只要满足条件就取元素,遇到不满足的立即停止SkipWhile:只要满足条件就跳过,遇到不满足的开始取后面所有元素
int[] nums = { 1, 2, 3, 4, 5, 1, 2 };
var takeWhile = nums.TakeWhile(x => x < 4); // 1, 2, 3
七、使用建议与常见坑点
- 优先使用方法语法:功能完整,链式调用清晰,是行业主流写法。
- 注意延迟执行副作用:不要在循环中反复枚举同一个 LINQ 查询,必要时先 ToList 缓存。
- EF Core 场景注意:LINQ to Entities 中,Where 条件会被翻译为 SQL,不能使用无法翻译的自定义方法,否则会抛出异常。
- Single 慎用:不确定数据数量时,优先用 FirstOrDefault 或 SingleOrDefault,避免不必要的异常。

浙公网安备 33010602011771号