05-LINQ查询语言入门

一、前言

LINQ(Language Integrated Query,语言集成查询)是 C# 中最强大的特性之一。它让你可以用统一的语法来查询各种数据源:数组、列表、数据库、XML 等等。

学会 LINQ,你的代码会变得更简洁、更易读。

二、什么是 LINQ?

2.1 用大白话解释

想象你有一个装满学生信息的文件柜,你想找出所有成绩大于 90 分的学生。

传统方式:一个一个翻,符合条件的拿出来。

LINQ 方式:直接说"给我所有成绩大于 90 分的学生",系统自动帮你找出来。

2.2 一个简单的例子

为了更直观地感受 LINQ 的优势,下面用一个最小的整数列表示例对比传统写法和 LINQ 写法。

csharp 复制代码
// 准备数据
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// 传统方式:找出所有偶数
List<int> evenNumbers1 = new List<int>();
foreach (var n in numbers)
{
    if (n % 2 == 0)
    {
        evenNumbers1.Add(n);
    }
}

// LINQ 方式:一行搞定
var evenNumbers2 = numbers.Where(n => n % 2 == 0).ToList();

这段代码先创建一个包含 1 到 10 的整数列表,然后用传统循环逐个判断是否为偶数并收集结果,最后再用 Where 进行条件筛选并用 ToList 立刻执行查询。对比能直观看到 LINQ 把"筛选条件"和"结果集合"表达得更清晰,避免了手写循环的样板代码。

三、LINQ 的两种写法

3.1 方法语法(推荐)
csharp 复制代码
var result = numbers
    .Where(n => n > 5)
    .OrderBy(n => n)
    .Select(n => n * 2);

这里展示了典型的链式调用风格,Where 先筛选出大于 5 的元素,OrderBy 再进行升序排序,最后 Select 把每个元素映射为原值的 2 倍。整个过程从"过滤→排序→转换"一步步读下来就像在描述需求,且每一步都保持可读性。

3.2 查询语法
csharp 复制代码
var result = from n in numbers
             where n > 5
             orderby n
             select n * 2;

查询语法和 SQL 的表达方式非常接近,from 指定数据源,where 指定条件,orderby 指定排序,select 指定投影结果。它最终仍会被编译器转换成方法语法的调用,因此两者只是写法不同,执行效果相同。

两种写法效果一样,方法语法更常用,因为更灵活。

四、Lambda 表达式

在学 LINQ 之前,先了解一下 Lambda 表达式,它是 LINQ 的基础。

4.1 什么是 Lambda?

Lambda 就是一种简写的匿名函数。

csharp 复制代码
// 普通方法
bool IsEven(int n)
{
    return n % 2 == 0;
}

// Lambda 表达式(做同样的事)
Func<int, bool> isEven = n => n % 2 == 0;

// 使用
Console.WriteLine(IsEven(4));    // true
Console.WriteLine(isEven(4));    // true

IsEven 是具名方法,接收一个整数并返回是否为偶数;isEven 则是同样逻辑的 Lambda 表达式,通过 Func<int, bool> 表明输入是 int,输出是 bool。最后两次调用分别展示了具名方法和 Lambda 都能以相同方式被执行。

4.2 Lambda 语法
csharp 复制代码
// 无参数
() => Console.WriteLine("Hello")

// 一个参数(可以省略括号)
x => x * 2
(x) => x * 2

// 多个参数
(x, y) => x + y

// 多条语句(需要大括号和 return)
(x, y) => {
    var sum = x + y;
    return sum;
}

这一组示例从无参到多参,再到包含多条语句的写法,逐步展示了 Lambda 的语法边界。只有一行表达式时可以省略大括号并隐式返回,遇到多行逻辑则必须使用大括号并显式 return,这样既保证可读性也避免语义歧义。

五、常用 LINQ 方法

让我们用一个学生列表来演示各种 LINQ 方法:

csharp 复制代码
// 准备测试数据
public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    public string Class { get; set; }
    public int Score { get; set; }
}

List<Student> students = new List<Student>
{
    new Student { Id = 1, Name = "张三", Age = 18, Class = "一班", Score = 85 },
    new Student { Id = 2, Name = "李四", Age = 19, Class = "一班", Score = 92 },
    new Student { Id = 3, Name = "王五", Age = 18, Class = "二班", Score = 78 },
    new Student { Id = 4, Name = "赵六", Age = 20, Class = "二班", Score = 95 },
    new Student { Id = 5, Name = "钱七", Age = 19, Class = "一班", Score = 88 },
    new Student { Id = 6, Name = "孙八", Age = 18, Class = "二班", Score = 72 }
};

这里先定义 Student 类型,包含编号、姓名、年龄、班级和分数等字段,便于后续通过 LINQ 进行筛选和统计。随后用集合初始化器构造了 6 条样例数据,使每个 LINQ 示例都能直接运行并看到结果。

5.1 Where:筛选
csharp 复制代码
// 找出成绩大于 80 分的学生
var goodStudents = students.Where(s => s.Score > 80);

foreach (var s in goodStudents)
{
    Console.WriteLine($"{s.Name}: {s.Score}分");
}
// 输出:张三: 85分, 李四: 92分, 赵六: 95分, 钱七: 88分

// 多条件筛选
var result = students.Where(s => s.Score > 80 && s.Class == "一班");

Where 接收一个返回布尔值的条件表达式,只保留满足条件的元素。第一段只筛选高分学生,第二段在此基础上增加班级条件,展示了多条件组合的写法与可读性。

5.2 Select:投影(转换)
csharp 复制代码
// 只获取学生姓名
var names = students.Select(s => s.Name);
// ["张三", "李四", "王五", ...]

// 转换成新的对象
var summaries = students.Select(s => new
{
    s.Name,
    Status = s.Score >= 60 ? "及格" : "不及格"
});

foreach (var item in summaries)
{
    Console.WriteLine($"{item.Name}: {item.Status}");
}

Select 用来"投影"数据结构,把完整对象转换成需要的形态。第一个示例只提取姓名,第二个示例创建匿名对象并新增 Status 字段,用分数判断是否及格,这样可以在不改动原始数据的前提下得到新的视图。

5.3 OrderBy / OrderByDescending:排序
csharp 复制代码
// 按成绩升序排列
var sortedAsc = students.OrderBy(s => s.Score);

// 按成绩降序排列
var sortedDesc = students.OrderByDescending(s => s.Score);

// 多字段排序:先按班级,再按成绩
var multiSort = students
    .OrderBy(s => s.Class)
    .ThenByDescending(s => s.Score);

OrderByOrderByDescending 控制排序方向,ThenBy 系列用于二级排序。这里先按班级分组,再在班级内部按成绩降序排列,常用于生成既分组又排序的报表结果。

5.4 First / FirstOrDefault:获取第一个
csharp 复制代码
// 获取第一个学生
var first = students.First();

// 获取第一个成绩大于 90 的学生
var topStudent = students.First(s => s.Score > 90);

// FirstOrDefault:如果没找到返回 null,不会报错
var notFound = students.FirstOrDefault(s => s.Score > 100);
// notFound 是 null

First 会直接取到序列中的第一个元素或满足条件的第一个元素,如果不存在会抛异常。FirstOrDefault 在未找到时返回默认值(引用类型为 null),更适合需要安全兜底的场景。

5.5 Single / SingleOrDefault:获取唯一一个
csharp 复制代码
// 获取 Id 为 1 的学生(必须有且只有一个)
var student = students.Single(s => s.Id == 1);

// SingleOrDefault:如果没找到返回 null
var notFound = students.SingleOrDefault(s => s.Id == 999);

Single 强调"必须且只能有一个",如果结果为空或多于一个都会抛异常,这能帮助你发现数据一致性问题。SingleOrDefault 则在找不到时返回默认值,但当存在多个匹配项时依然会抛异常。

5.6 Any / All:判断
csharp 复制代码
// 是否有成绩大于 90 的学生
bool hasTopStudent = students.Any(s => s.Score > 90);  // true

// 是否所有学生都及格
bool allPassed = students.All(s => s.Score >= 60);  // true

Any 用于判断是否存在任意满足条件的元素,All 用于判断是否全部满足条件。它们返回布尔值,经常用于业务规则判断或快速短路检查。

5.7 Count:计数
csharp 复制代码
// 学生总数
int total = students.Count();  // 6

// 一班的学生数
int class1Count = students.Count(s => s.Class == "一班");  // 3

Count 会统计元素数量,也可以带条件,只计算满足条件的元素数量。它比先筛选再数数量更直接,也更符合"读起来像需求"的写法。

5.8 Sum / Average / Max / Min:聚合
csharp 复制代码
// 总分
int totalScore = students.Sum(s => s.Score);

// 平均分
double avgScore = students.Average(s => s.Score);

// 最高分
int maxScore = students.Max(s => s.Score);

// 最低分
int minScore = students.Min(s => s.Score);

// 获取最高分的学生
var topStudent = students.MaxBy(s => s.Score);
Console.WriteLine($"最高分:{topStudent.Name} - {topStudent.Score}分");

这些方法属于聚合操作,会把一组数据压缩为单个结果。SumAverageMaxMin 直接对数值字段计算,MaxBy 则返回"拥有最大值的对象本身",适合在统计之后还要用到对象信息的场景。

5.9 GroupBy:分组
csharp 复制代码
// 按班级分组
var groups = students.GroupBy(s => s.Class);

foreach (var group in groups)
{
    Console.WriteLine($"\n{group.Key}:");
    foreach (var student in group)
    {
        Console.WriteLine($"  {student.Name}: {student.Score}分");
    }
}

// 输出:
// 一班:
//   张三: 85分
//   李四: 92分
//   钱七: 88分
// 二班:
//   王五: 78分
//   赵六: 95分
//   孙八: 72分

// 分组统计
var classStats = students
    .GroupBy(s => s.Class)
    .Select(g => new
    {
        ClassName = g.Key,
        Count = g.Count(),
        AvgScore = g.Average(s => s.Score),
        MaxScore = g.Max(s => s.Score)
    });

foreach (var stat in classStats)
{
    Console.WriteLine($"{stat.ClassName}: {stat.Count}人, 平均分{stat.AvgScore:F1}, 最高分{stat.MaxScore}");
}

GroupBy 会把相同键的元素组织为一个分组序列,然后可以针对每组做进一步统计。这里先按班级输出分组结果,再通过 Select 计算每组人数、平均分和最高分,形成结构化的统计报表。

5.10 Take / Skip:分页
csharp 复制代码
// 取前 3 个
var top3 = students.Take(3);

// 跳过前 2 个
var skip2 = students.Skip(2);

// 分页:第 2 页,每页 2 条
int pageSize = 2;
int pageNumber = 2;
var page2 = students
    .Skip((pageNumber - 1) * pageSize)
    .Take(pageSize);

TakeSkip 是最常见的分页组合,Skip 用于跳过前面若干条记录,Take 取出指定数量。第二页的计算采用 (pageNumber - 1) * pageSize 作为偏移量,这是分页公式的标准写法。

5.11 Distinct:去重
csharp 复制代码
// 获取所有不重复的班级
var classes = students.Select(s => s.Class).Distinct();
// ["一班", "二班"]

Distinct 会去掉重复元素,因此常与 Select 搭配先提取某个字段,再获得唯一值集合。这样可以快速得到所有班级、所有年份或所有分类等唯一列表。

5.12 Join:连接
csharp 复制代码
// 准备另一个数据源
var classInfos = new[]
{
    new { ClassName = "一班", Teacher = "王老师" },
    new { ClassName = "二班", Teacher = "李老师" }
};

// 连接查询
var joined = students.Join(
    classInfos,
    student => student.Class,
    classInfo => classInfo.ClassName,
    (student, classInfo) => new
    {
        student.Name,
        student.Class,
        classInfo.Teacher
    }
);

foreach (var item in joined)
{
    Console.WriteLine($"{item.Name} - {item.Class} - {item.Teacher}");
}

Join 会按照指定的键把两个序列连接起来,形成新的结果集。这里用学生的 Class 与班级信息的 ClassName 对齐,最后在结果中同时包含学生姓名、班级和班主任,实现"数据合并"的效果。

六、链式调用

LINQ 的强大之处在于可以链式调用多个方法:

csharp 复制代码
// 找出一班成绩前 2 名的学生姓名
var result = students
    .Where(s => s.Class == "一班")      // 筛选一班
    .OrderByDescending(s => s.Score)    // 按成绩降序
    .Take(2)                            // 取前 2 个
    .Select(s => s.Name);               // 只要姓名

// 结果:["李四", "钱七"]

这个例子把筛选、排序、取前 N 条和投影组合在一起,是 LINQ 链式调用的典型模式。每一步都明确表达业务意图,最终结果只保留姓名列表,既简洁又易维护。

七、延迟执行

LINQ 有一个重要特性:延迟执行。查询不会立即执行,而是在你真正需要结果时才执行。

csharp 复制代码
// 这里只是定义查询,还没有执行
var query = students.Where(s => s.Score > 80);

// 添加一个新学生
students.Add(new Student { Id = 7, Name = "周九", Age = 19, Class = "一班", Score = 90 });

// 现在才执行查询,会包含新添加的学生
foreach (var s in query)
{
    Console.WriteLine(s.Name);
}
// 输出会包含"周九"!

延迟执行意味着查询表达式先被"定义",只有在枚举结果时才真正执行。因为在执行前插入了新学生,所以最终遍历时会包含新数据,这能帮助你理解 LINQ 在内存集合上的执行时机。

如果想立即执行,使用 ToList()ToArray() 等方法:

csharp 复制代码
// 立即执行并保存结果
var result = students.Where(s => s.Score > 80).ToList();

ToList 会立刻执行查询并把结果固化成一个新列表,此后即使原集合变化,已生成的列表也不会受影响,适合需要稳定快照的场景。

八、实战案例:订单数据分析

下面用一组订单数据完成常见的统计需求,包含订单金额计算、客户消费汇总、销量统计和日期筛选。这个案例会把筛选、分组、聚合、排序等操作串起来,帮助你理解这些方法在真实业务中的组合方式。

csharp 复制代码
public class Order
{
    public int Id { get; set; }
    public string CustomerName { get; set; }
    public string Product { get; set; }
    public decimal Price { get; set; }
    public int Quantity { get; set; }
    public DateTime OrderDate { get; set; }
}

List<Order> orders = new List<Order>
{
    new Order { Id = 1, CustomerName = "张三", Product = "手机", Price = 3999, Quantity = 1, OrderDate = new DateTime(2024, 1, 15) },
    new Order { Id = 2, CustomerName = "李四", Product = "耳机", Price = 299, Quantity = 2, OrderDate = new DateTime(2024, 1, 16) },
    new Order { Id = 3, CustomerName = "张三", Product = "充电器", Price = 99, Quantity = 3, OrderDate = new DateTime(2024, 1, 17) },
    new Order { Id = 4, CustomerName = "王五", Product = "手机", Price = 4999, Quantity = 1, OrderDate = new DateTime(2024, 1, 18) },
    new Order { Id = 5, CustomerName = "李四", Product = "手机壳", Price = 49, Quantity = 5, OrderDate = new DateTime(2024, 1, 19) },
    new Order { Id = 6, CustomerName = "张三", Product = "数据线", Price = 29, Quantity = 10, OrderDate = new DateTime(2024, 1, 20) }
};

// 1. 计算每个订单的总金额
var orderTotals = orders.Select(o => new
{
    o.Id,
    o.CustomerName,
    o.Product,
    Total = o.Price * o.Quantity
});

Console.WriteLine("=== 订单明细 ===");
foreach (var order in orderTotals)
{
    Console.WriteLine($"订单{order.Id}: {order.CustomerName} 购买 {order.Product},金额:{order.Total:C}");
}

// 2. 统计每个客户的消费总额
var customerStats = orders
    .GroupBy(o => o.CustomerName)
    .Select(g => new
    {
        Customer = g.Key,
        OrderCount = g.Count(),
        TotalAmount = g.Sum(o => o.Price * o.Quantity)
    })
    .OrderByDescending(x => x.TotalAmount);

Console.WriteLine("\n=== 客户消费统计 ===");
foreach (var stat in customerStats)
{
    Console.WriteLine($"{stat.Customer}: {stat.OrderCount}笔订单,消费总额:{stat.TotalAmount:C}");
}

// 3. 找出消费最多的客户
var topCustomer = customerStats.First();
Console.WriteLine($"\n消费最多的客户:{topCustomer.Customer},消费:{topCustomer.TotalAmount:C}");

// 4. 统计每种商品的销量
var productStats = orders
    .GroupBy(o => o.Product)
    .Select(g => new
    {
        Product = g.Key,
        TotalQuantity = g.Sum(o => o.Quantity),
        TotalRevenue = g.Sum(o => o.Price * o.Quantity)
    })
    .OrderByDescending(x => x.TotalRevenue);

Console.WriteLine("\n=== 商品销售统计 ===");
foreach (var stat in productStats)
{
    Console.WriteLine($"{stat.Product}: 销量{stat.TotalQuantity},销售额:{stat.TotalRevenue:C}");
}

// 5. 查询特定日期范围的订单
var startDate = new DateTime(2024, 1, 16);
var endDate = new DateTime(2024, 1, 18);
var dateRangeOrders = orders
    .Where(o => o.OrderDate >= startDate && o.OrderDate <= endDate)
    .OrderBy(o => o.OrderDate);

Console.WriteLine($"\n=== {startDate:d} 到 {endDate:d} 的订单 ===");
foreach (var order in dateRangeOrders)
{
    Console.WriteLine($"{order.OrderDate:d}: {order.CustomerName} - {order.Product}");
}

这段代码先用 Order 类定义订单字段,包含客户、商品、价格、数量和日期等核心信息,再用集合初始化器构造示例订单,便于后续统计直接运行。第一步用 Select 做投影计算,把 Price * Quantity 得到每笔订单金额,并输出明细,这里强调"原始数据不变,派生结果单独生成"。第二步对客户名称进行 GroupBy,在分组结果上通过 CountSum 计算订单数与消费总额,再用 OrderByDescending 把高消费客户排在前面,让报表更直观。第三步使用 First 取出消费最多的客户,因为前一步已排序,所以第一条就是目标结果。第四步对商品进行分组,并分别统计销量和销售额,这里的 Sum 用在数量和金额上,体现了同一分组多维统计的写法。第五步通过 Where 过滤日期区间,再 OrderBy 按时间排序,得到某段时间内的订单清单,适合生成周期报表或趋势分析。整体流程展示了 LINQ 在真实业务中"先筛选、再分组、再聚合、最后排序"的典型用法。

九、小结

这篇文章我们学习了 LINQ 的核心内容:

我们从 Lambda 表达式开始,理解了匿名函数如何作为查询条件和投影逻辑被复用,随后掌握了筛选、投影、排序、聚合、分组、分页与判断等常用操作,并通过延迟执行理解查询的执行时机。这些能力组合起来,就能用简洁清晰的语句完成常见的数据处理任务。

十、下一篇预告

下一篇我们将进入进阶开发部分,学习 ASP.NET Core Web API 开发,开始构建真正的后端服务。


练习题

  1. 给定一个整数列表,找出所有偶数,平方后,按降序排列
  2. 统计一段文本中每个单词出现的次数
  3. 实现一个简单的学生成绩管理系统,支持按班级、按成绩范围查询
相关推荐
钰fly6 小时前
工具块与vs的联合编程(豆包总结生成)
c#
c#上位机7 小时前
wpf之行为
c#·wpf
星夜泊客7 小时前
C# 基础:为什么类可以在静态方法中创建自己的实例?
开发语言·经验分享·笔记·unity·c#·游戏引擎
kylezhao20199 小时前
深入浅出地理解 C# WPF 中的属性
hadoop·c#·wpf
多多*9 小时前
2月3日面试题整理 字节跳动后端开发相关
android·java·开发语言·网络·jvm·adb·c#
一念春风10 小时前
C# 通用工具类代码
c#
海盗123410 小时前
WPF上位机组件开发-设备状态运行图基础版
开发语言·c#·wpf
浮生如梦_12 小时前
C# 窗体工厂类 - 简单工厂模式演示案例
计算机视觉·c#·视觉检测·简单工厂模式
两千次12 小时前
web主从站
windows·c#