一、前言
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);
OrderBy 和 OrderByDescending 控制排序方向,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}分");
这些方法属于聚合操作,会把一组数据压缩为单个结果。Sum、Average、Max、Min 直接对数值字段计算,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);
Take 和 Skip 是最常见的分页组合,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,在分组结果上通过 Count 和 Sum 计算订单数与消费总额,再用 OrderByDescending 把高消费客户排在前面,让报表更直观。第三步使用 First 取出消费最多的客户,因为前一步已排序,所以第一条就是目标结果。第四步对商品进行分组,并分别统计销量和销售额,这里的 Sum 用在数量和金额上,体现了同一分组多维统计的写法。第五步通过 Where 过滤日期区间,再 OrderBy 按时间排序,得到某段时间内的订单清单,适合生成周期报表或趋势分析。整体流程展示了 LINQ 在真实业务中"先筛选、再分组、再聚合、最后排序"的典型用法。
九、小结
这篇文章我们学习了 LINQ 的核心内容:
我们从 Lambda 表达式开始,理解了匿名函数如何作为查询条件和投影逻辑被复用,随后掌握了筛选、投影、排序、聚合、分组、分页与判断等常用操作,并通过延迟执行理解查询的执行时机。这些能力组合起来,就能用简洁清晰的语句完成常见的数据处理任务。
十、下一篇预告
下一篇我们将进入进阶开发部分,学习 ASP.NET Core Web API 开发,开始构建真正的后端服务。
练习题:
- 给定一个整数列表,找出所有偶数,平方后,按降序排列
- 统计一段文本中每个单词出现的次数
- 实现一个简单的学生成绩管理系统,支持按班级、按成绩范围查询