
C# 语法糖之 LINQ
LINQ 是什么?
不过多赘述,引用来自 MSDN 上的介绍。
Language-Integrated 查询(LINQ)是基于将查询功能直接集成到 C# 语言的一组技术的名称。 传统上,针对数据的查询表示为简单字符串,无需在编译时进行类型检查或 IntelliSense 支持。 此外,必须了解每种数据源类型的不同查询语言:SQL 数据库、XML 文档、各种 Web 服务等。 使用 LINQ 时,查询是一流的语言构造,就像类、方法和事件一样。
编写查询时,LINQ 最明显的“语言集成”部分是查询表达式。 查询表达式以声明性 查询语法编写。 通过使用查询语法,可以使用最少的代码对数据源执行筛选、排序和分组作。 使用相同的查询表达式模式从任何类型的数据源查询和转换数据。
基本语法
查询表达式(Query Syntax)
int[] numbers = [1, 3, 5, 7, 9, 10, 20];
var evenNumbers =
from n in numbers
where n % 2 == 0
orderby n descending
select n;
foreach (var num in evenNumbers)
Console.WriteLine(num);
from
:指定数据源where
:过滤条件orderby
:排序select
:投影(选择字段或新类型)
方法链(Method Syntax)
int[] numbers = [1, 3, 5, 7, 9, 10, 20];
var evenNumbers = numbers
.Where(n => n % 2 == 0)
.OrderByDescending(n => n);
foreach (var num in evenNumbers)
Console.WriteLine(num);
Where
、Select
、OrderBy
等都是定义在 System.Linq.Enumerable
(针对内存集合)或 Queryable
(针对可查询提供者,如 EF)的扩展方法。
个人比较偏向使用链式调用,感觉更明显直接,所以接下来所有代码都以链式调用为示例。
执行机制
延迟执行(Deferred Execution)
LINQ 查询表达式本身不会立即执行,只有在对结果进行枚举(foreach
、.ToList()
、.ToArray()
、.Count()
等操作)时才真正遍历数据源并执行逻辑。
即时执行(Immediate Execution)
某些方法会立即触发查询:
- 聚合操作:
.Count()
,.Sum()
,.Max()
,.Average()
- 转换为集合:
.ToList()
,.ToArray()
,.ToDictionary()
int[] numbers = [1, 3, 5, 7, 9, 10, 20];
var query = numbers.Where(n => n > 5); // 不执行
Console.WriteLine("定义完成");
var list = query.ToList(); // 现在才遍历执行
Console.WriteLine("执行完成,共有 " + list.Count + " 个元素");
操作符分类
分类 | 方法示例 | 说明 |
---|---|---|
过滤 | .Where(...) |
条件过滤 |
投影 | .Select(...) ,.SelectMany(...) |
映射到新类型或扁平化集合 |
排序 | .OrderBy(...) ,.ThenBy(...) |
升序、降序排序 |
分组 | .GroupBy(...) |
按键分组 |
聚合 | .Count() ,.Sum() ,.Max() ,.Min() ,.Average() |
聚合计算 |
分区 | .Skip(n) ,.Take(n) |
跳过前 n 项或获取前 n 项 |
连接 | .Join(...) ,.GroupJoin(...) ,.Zip(...) |
内连接、外连接、拉链 |
元素 | .First() ,.FirstOrDefault() ,.Single() ,.ElementAt() |
获取单个元素 |
集合运算 | .Distinct() ,.Union() ,.Intersect() ,.Except() |
去重、并集、交集、差集 |
转换 | .ToList() ,.ToArray() ,.ToDictionary() |
转换为不同集合类型 |
典型示例
分组与聚合
var orders = new[]
{
new Order { Customer = "张三", Amount = 100 },
new Order { Customer = "李四", Amount = 200 },
new Order { Customer = "王五", Amount = 650 },
new Order { Customer = "张三", Amount = 300 },
new Order { Customer = "李四", Amount = 250 },
new Order { Customer = "李四", Amount = 100 }
};
var result = orders.GroupBy(o => o.Customer)
.Select(g => new
{
Customer = g.Key, TotalAmount = g.Sum(o => o.Amount), Count = g.Count()
})
.OrderBy(x => x.TotalAmount);
foreach (var order in result)
Console.WriteLine($"姓名: {order.Customer}, 总消费: {order.TotalAmount}, 次数: {order.Count}");
class Order
{
public string? Customer { get; set; }
public int Amount { get; set; }
}
// 按 总消费 顺序输出:
// 姓名: 张三, 总消费: 400, 次数: 2
// 姓名: 李四, 总消费: 550, 次数: 3
// 姓名: 王五, 总消费: 650, 次数: 1
多数据源连接
Student[] students =
[
new() { Id = 1, Name = "张三" },
new() { Id = 2, Name = "李四" },
new() { Id = 3, Name = "王五" }
];
Score[] scores = [
new() { StudentId = 1, Value = 90 },
new() { StudentId = 2, Value = 85 },
new() { StudentId = 3, Value = 78 }
];
var query = students
.Join(scores, s => s.Id, sc => sc.StudentId, (s, sc) => new { s.Name, sc.Value });
query.ToList().ForEach(item =>
Console.WriteLine($"{item.Name} 的成绩是 {item.Value}")
);
class Student
{
public int Id;
public string? Name;
}
class Score
{
public int StudentId;
public int Value;
}
// 输出:
// 张三 的成绩是 90
// 李四 的成绩是 85
// 王五 的成绩是 78
提供程序(LINQ Provider)与表达式树
LINQ Provider 实现了核心接口 IQueryProvider
,负责将构造好的表达式树(Expression
)解析、翻译并执行到目标数据源上。
关键接口
public interface IQueryProvider
{
IQueryable<TElement> CreateQuery<TElement>(Expression expression);
TResult Execute<TResult>(Expression expression);
}
工作流程
- 表达式树构建:所有对
IQueryable<T>
的扩展方法调用都会累积成一棵包含查询操作的表达式树。 - 解析与翻译:查询提供程序遍历表达式树,将节点映射为目标后端的查询指令(如 T-SQL、XPath 等)或本地迭代逻辑。
- 执行与映射:执行翻译生成的查询,并将返回的原始数据映射为 CLR 对象或基本类型。
常见实现
Provider | 接口/类型 | 数据源类型 |
---|---|---|
LINQ to Objects | Enumerable +IEnumerable |
内存集合 |
LINQ to Entities (EF) | EntityQueryProvider +IQueryable |
关系型数据库 (SQL) |
LINQ to XML | XContainer 查询方法 |
XML 文档 |
PLINQ | ParallelQuery |
并行处理的内存集合 |
自定义 Provider | 自定义 IQueryProvider |
任意后端(REST API、自有缓存等) |
性能优化
- 缓存查询结果:对同一查询多次枚举时,使用
.ToList()
或.ToArray()
缓存结果,避免重复计算。 - 合理分批加载:结合
.Skip(n).Take(m)
分页或批处理,减少单次数据量。 - 最小化投影字段:在 EF Core、LINQ to SQL 中,仅选择所需字段以减少网络与内存开销。
- 表达式复用:将常用过滤、排序表达式封装为
Expression<Func<T, bool>>
以便重用。 - 谨慎使用 PLINQ:仅对 CPU 密集型、可无序分区的内存集合并行处理,避免在 I/O 密集型场景或需保序逻辑中使用。
- 感谢你赐予我前进的力量
赞赏者名单
因为你们的支持让我意识到写文章的价值🙏
本文是原创文章,采用 CC BY-NC-ND 4.0 协议,完整转载请注明来自 Swaggy Macro
评论
匿名评论
隐私政策
你无需删除空行,直接评论以获取最佳展示效果