Decade
Decade
Published on 2025-07-16 / 44 Visits
0
0

LinQ

.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
]

执行步骤:

  1. Where 后:4 条记录(id=3,4,5,6)

  2. GroupBy 后:3 个组(age=25, 30, 35)

  3. OrderBy 后:按年龄排序(25 → 30 → 35)

  4. Take(3) 后:保留 3 个组(全部保留)

  5. 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

核心技术点

  1. 链式调用:LINQ 方法返回 IEnumerable,可以连续调用

  2. 延迟执行:查询在 ToList() 时才真正执行

  3. 分组聚合GroupBy 后可以对组进行 Count()Average() 等操作

  4. 投影转换Select 创建新的数据结构

  5. 流式处理:数据像流水线一样依次经过各个操作


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,可以继续调用其他方法

  • 像搭积木一样构建复杂查询逻辑

学习建议

基础练习

  1. 单一操作熟练

    • 分别练习 WhereSelectOrderBy,确保熟悉每个方法

    • 尝试不同的条件表达式和投影方式

  2. 组合练习

    • 尝试组合 Where + Select:筛选后投影

    • 尝试组合 Where + OrderBy + Take:筛选、排序、分页

    • 尝试 GroupBy + Select:分组后统计

进阶练习

  1. 复杂查询

    • 多层嵌套的分组

    • 联合查询(Join

    • 子查询(在 SelectWhere 中使用 LINQ)

  2. 性能优化

    • 理解延迟执行与立即执行的区别

    • 避免在循环中重复执行查询

    • 学习使用 AsEnumerable()AsQueryable()

  3. 实战应用

    • 结合 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);


Comment