.NET 中的 LINQ 基础与进阶应用
1. LINQ 基础介绍
什么是 LINQ?
LINQ(Language Integrated Query,语言集成查询)是 .NET Framework 3.5 引入的一项强大功能,它允许开发者使用统一的语法对不同数据源(如集合、数据库、XML 等)进行查询操作。
主要作用
统一查询语法:无论数据来自内存集合、数据库还是 XML,都可以使用相同的查询方式
类型安全:编译时检查,减少运行时错误
提高代码可读性:使用声明式语法,代码更简洁易懂
延迟执行:查询在真正需要结果时才执行,提高性能
常见使用场景
对集合进行筛选、排序、分组
数据库查询(LINQ to SQL、Entity Framework)
XML 文档处理(LINQ to XML)
数据转换与投影
复杂的聚合计算
2. 常用查询方法讲解
2.1 Where - 条件筛选
用途:根据指定条件筛选集合中的元素。
csharp
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// 筛选出所有偶数
var evenNumbers = numbers.Where(n => n % 2 == 0);
// 结果: 2, 4, 6, 8, 10
2.2 Count - 计数
用途:返回集合中元素的数量,可以带条件。
csharp
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// 获取总数量
int total = numbers.Count(); // 结果: 10
// 获取满足条件的元素数量
int evenCount = numbers.Count(n => n % 2 == 0); // 结果: 5
2.3 Any - 存在性检查
用途:判断集合中是否存在满足条件的元素,返回布尔值。
csharp
List<int> numbers = new List<int> { 1, 3, 5, 7, 9 };
// 检查是否存在偶数
bool hasEven = numbers.Any(n => n % 2 == 0); // 结果: false
// 检查集合是否为空
bool hasElements = numbers.Any(); // 结果: true
2.4 Single 与 First 系列
Single vs First
方法用途无元素时多个元素时Single()返回唯一元素抛出异常抛出异常SingleOrDefault()返回唯一元素或默认值返回 default抛出异常First()返回第一个元素抛出异常返回第一个FirstOrDefault()返回第一个元素或默认值返回 default返回第一个
示例代码:
csharp
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
List<int> empty = new List<int>();
List<int> single = new List<int> { 42 };
// Single - 期望集合中只有一个元素
int uniqueValue = single.Single(); // 结果: 42
// numbers.Single(); // 会抛出异常,因为有多个元素
// SingleOrDefault - 如果为空返回默认值
int defaultValue = empty.SingleOrDefault(); // 结果: 0
// First - 返回第一个元素
int firstValue = numbers.First(); // 结果: 1
int firstEven = numbers.First(n => n % 2 == 0); // 结果: 2
// FirstOrDefault - 为空时返回默认值
int firstOrDefault = empty.FirstOrDefault(); // 结果: 0
使用建议:
当确定集合中只有一个元素时,使用
Single()
当需要第一个元素时,使用
First()
当可能为空且需要处理默认值时,使用
OrDefault
版本
2.5 OrderBy 与 ThenBy - 排序
用途:
OrderBy
:按指定键进行升序排序(第一优先级)ThenBy
:在前一个排序基础上进行次级排序
csharp
class Person
{
public string Name { get; set; }
public int Age { get; set; }
public decimal Salary { get; set; }
}
List<Person> people = new List<Person>
{
new Person { Name = "张三", Age = 25, Salary = 5000 },
new Person { Name = "李四", Age = 30, Salary = 6000 },
new Person { Name = "王五", Age = 25, Salary = 5500 }
};
// 先按年龄排序,年龄相同时按薪资排序
var sorted = people.OrderBy(p => p.Age)
.ThenBy(p => p.Salary);
// 降序使用 OrderByDescending 和 ThenByDescending
var sortedDesc = people.OrderByDescending(p => p.Age)
.ThenByDescending(p => p.Salary);
2.6 Take 与 Skip - 分页操作
用途:
Take(n)
:获取前 n 个元素Skip(n)
:跳过前 n 个元素
csharp
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// 获取前 3 个元素
var first3 = numbers.Take(3); // 结果: 1, 2, 3
// 跳过前 5 个元素
var after5 = numbers.Skip(5); // 结果: 6, 7, 8, 9, 10
// 分页示例:获取第 2 页数据(每页 3 条)
int pageSize = 3;
int pageNumber = 2;
var page2 = numbers.Skip((pageNumber - 1) * pageSize).Take(pageSize);
// 结果: 4, 5, 6
2.7 聚合函数
用途:对集合进行数学计算。
csharp
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
// 最大值
int max = numbers.Max(); // 结果: 5
// 最小值
int min = numbers.Min(); // 结果: 1
// 平均值
double avg = numbers.Average(); // 结果: 3.0
// 总和
int sum = numbers.Sum(); // 结果: 15
// 对象集合的聚合
List<Person> people = new List<Person>
{
new Person { Name = "张三", Salary = 5000 },
new Person { Name = "李四", Salary = 6000 }
};
decimal maxSalary = people.Max(p => p.Salary); // 结果: 6000
decimal avgSalary = people.Average(p => p.Salary); // 结果: 5500
2.8 Select - 投影转换
用途:将集合中的每个元素转换为另一种形式。
csharp
List<Person> people = new List<Person>
{
new Person { Name = "张三", Age = 25, Salary = 5000 },
new Person { Name = "李四", Age = 30, Salary = 6000 }
};
// 提取单个属性
var names = people.Select(p => p.Name);
// 结果: "张三", "李四"
// 投影到匿名对象
var summary = people.Select(p => new
{
姓名 = p.Name,
年薪 = p.Salary * 12
});
// 投影到新类型
var cards = people.Select(p => new EmployeeCard
{
EmployeeName = p.Name,
BadgeNumber = p.Age + 1000
});
3. 默认值处理(Default 系列)
DefaultIfEmpty() 方法
用途:当集合为空时,返回一个包含默认值的集合,避免在后续操作中出现空集合问题。
csharp
List<int> empty = new List<int>();
List<int> numbers = new List<int> { 1, 2, 3 };
// 空集合使用默认值
var result1 = empty.DefaultIfEmpty();
// 结果: 包含一个元素 0 的集合
// 指定默认值
var result2 = empty.DefaultIfEmpty(99);
// 结果: 包含一个元素 99 的集合
// 非空集合不受影响
var result3 = numbers.DefaultIfEmpty();
// 结果: 1, 2, 3
xxxOrDefault() 系列方法
这类方法在找不到元素时返回类型的默认值,而不是抛出异常。
方法找不到元素时的行为FirstOrDefault()返回 default(T)LastOrDefault()返回 default(T)SingleOrDefault()返回 default(T)ElementAtOrDefault(n)返回 default(T)
csharp
List<int> numbers = new List<int> { 1, 2, 3 };
List<string> names = new List<string>();
// 引用类型的默认值是 null
string firstName = names.FirstOrDefault(); // 结果: null
// 值类型的默认值是 0、false 等
int firstNumber = new List<int>().FirstOrDefault(); // 结果: 0
// 带条件查询
int firstEven = numbers.FirstOrDefault(n => n % 2 == 0); // 结果: 2
int firstLarge = numbers.FirstOrDefault(n => n > 10); // 结果: 0
应用场景:
避免在查询空集合时抛出异常
在左连接(Left Join)操作中处理右侧无匹配数据的情况
提供安全的默认值处理逻辑
4. 分组与聚合示例
示例代码分析
csharp
var result = pp.GroupBy(a => a.age)
.Select(g => new { GuanJian = g.Key, SetName = g.Select(n => n.name) });
foreach (var item in result)
{
foreach (var VARIABLE in item.SetName)
{
Console.WriteLine(VARIABLE);
}
}
逐行讲解
第 1 行:pp.GroupBy(a => a.age)
作用:将集合
pp
按照age
属性进行分组结果:返回
IEnumerable<IGrouping<int, Person>>
类型理解:相同年龄的人会被分到同一组,每组是一个
IGrouping
对象
第 2 行:.Select(g => new { ... })
作用:对每个分组进行投影转换
参数 g:代表每一个分组(
IGrouping<int, Person>
)g.Key:分组的键值,即当前组的年龄
g.Select(n => n.name):从当前组中提取所有人的姓名
投影结果
创建了一个匿名对象,包含:
GuanJian
:该组的年龄值SetName
:该年龄组所有人的姓名集合
执行结果示例
假设原始数据:
csharp
pp = [
{ id=1, name="张三", age=25 },
{ id=2, name="李四", age=30 },
{ id=3, name="王五", age=25 },
{ id=4, name="赵六", age=30 }
]
执行后 result
的结构:
[
{ GuanJian = 25, SetName = ["张三", "王五"] },
{ GuanJian = 30, SetName = ["李四", "赵六"] }
]
输出结果
张三
王五
李四
赵六
关键概念
GroupBy
:返回的每个元素是一个分组,包含Key
(分组键)和元素集合IGrouping<TKey, TElement>
:分组接口,既有Key
属性,又实现了IEnumerable<TElement>
嵌套
Select
:外层Select
处理分组,内层Select
处理组内元素
5. LINQ 综合练习
示例代码
csharp
var l = pp.Where(a => a.id > 2)
.GroupBy(s => s.age)
.OrderBy(g => g.Key)
.Take(3)
.Select(o => new
{
NianLing = o.Key,
RenShu = o.Count(),
PingJunGongZi = o.Average(e => e.salary)
})
.ToList();
foreach (var item in l)
{
Console.WriteLine(item.NianLing);
Console.WriteLine(item.RenShu);
Console.WriteLine(item.PingJunGongZi);
}
执行流程详解
第 1 步:.Where(a => a.id > 2)
- 筛选
作用:过滤出
id
大于 2 的记录输入:原始集合
pp
输出:符合条件的子集
第 2 步:.GroupBy(s => s.age)
- 分组
作用:将筛选后的数据按
age
分组输入:Where 的结果集
输出:按年龄分组的集合,每组包含相同年龄的所有元素
第 3 步:.OrderBy(g => g.Key)
- 排序
作用:将分组按照年龄(Key)升序排列
输入:未排序的分组集合
输出:按年龄从小到大排序的分组集合
第 4 步:.Take(3)
- 限制数量
作用:只取前 3 个分组
输入:排序后的分组集合
输出:最多 3 个年龄最小的分组
第 5 步:.Select(o => new { ... })
- 投影与聚合
作用:将每个分组转换为统计信息对象
o.Key:当前组的年龄
o.Count():统计该年龄组的人数
o.Average(e => e.salary):计算该年龄组的平均工资
第 6 步:.ToList()
- 立即执行
作用:将查询结果物化为
List
集合重要性:LINQ 默认延迟执行,
ToList()
会立即执行查询
数据流转示例
假设原始数据:
csharp
pp = [
{ id=1, age=25, salary=5000 },
{ id=2, age=30, salary=6000 },
{ id=3, age=25, salary=5500 }, // ✓ id > 2
{ id=4, age=30, salary=6500 }, // ✓ id > 2
{ id=5, age=35, salary=7000 }, // ✓ id > 2
{ id=6, age=25, salary=5800 } // ✓ id > 2
]
执行步骤:
Where 后:4 条记录(id=3,4,5,6)
GroupBy 后:3 个组(age=25, 30, 35)
OrderBy 后:按年龄排序(25 → 30 → 35)
Take(3) 后:保留 3 个组(全部保留)
Select 后:
[
{ NianLing=25, RenShu=2, PingJunGongZi=5650 }, // (5500+5800)/2
{ NianLing=30, RenShu=1, PingJunGongZi=6500 },
{ NianLing=35, RenShu=1, PingJunGongZi=7000 }
]
输出结果
25
2
5650
30
1
6500
35
1
7000
核心技术点
链式调用:LINQ 方法返回
IEnumerable
,可以连续调用延迟执行:查询在
ToList()
时才真正执行分组聚合:
GroupBy
后可以对组进行Count()
、Average()
等操作投影转换:
Select
创建新的数据结构流式处理:数据像流水线一样依次经过各个操作
6. 总结与学习建议
LINQ 核心思维方式
1. 声明式查询
传统命令式:告诉计算机"怎么做"(循环、条件判断)
LINQ 声明式:告诉计算机"要什么"(筛选条件、排序规则)
csharp
// 命令式
List<int> result = new List<int>();
foreach (var num in numbers)
{
if (num % 2 == 0)
result.Add(num);
}
// 声明式(LINQ)
var result = numbers.Where(n => n % 2 == 0);
2. 延迟执行(Deferred Execution)
查询定义时不会立即执行
在迭代结果(
foreach
)或调用ToList()
、ToArray()
时才执行好处:可以构建复杂查询,最后一次性优化执行
csharp
var query = numbers.Where(n => n > 5); // 此时未执行
// ... 可能添加更多筛选条件
var result = query.ToList(); // 此时才真正执行查询
3. 链式调用与组合
每个 LINQ 方法返回
IEnumerable
,可以继续调用其他方法像搭积木一样构建复杂查询逻辑
学习建议
基础练习
单一操作熟练
分别练习
Where
、Select
、OrderBy
,确保熟悉每个方法尝试不同的条件表达式和投影方式
组合练习
尝试组合
Where
+Select
:筛选后投影尝试组合
Where
+OrderBy
+Take
:筛选、排序、分页尝试
GroupBy
+Select
:分组后统计
进阶练习
复杂查询
多层嵌套的分组
联合查询(
Join
)子查询(在
Select
或Where
中使用 LINQ)
性能优化
理解延迟执行与立即执行的区别
避免在循环中重复执行查询
学习使用
AsEnumerable()
和AsQueryable()
实战应用
结合 Entity Framework 进行数据库查询
处理 JSON 数据
生成报表和统计信息
推荐练习题目
csharp
// 练习1:找出年龄大于25岁、工资前3高的员工姓名
var result1 = employees
.Where(e => e.Age > 25)
.OrderByDescending(e => e.Salary)
.Take(3)
.Select(e => e.Name);
// 练习2:按部门分组,统计每个部门的平均工资和人数
var result2 = employees
.GroupBy(e => e.Department)
.Select(g => new
{
Department = g.Key,
AvgSalary = g.Average(e => e.Salary),
Count = g.Count()
});
// 练习3:找出工资高于平均工资的员工
var avgSalary = employees.Average(e => e.Salary);
var result3 = employees.Where(e => e.Salary > avgSalary);