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);

WhereSelectOrderBy 等都是定义在 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、自有缓存等)

性能优化

  1. 缓存查询结果:对同一查询多次枚举时,使用 .ToList().ToArray() 缓存结果,避免重复计算。
  2. 合理分批加载:结合 .Skip(n).Take(m) 分页或批处理,减少单次数据量。
  3. 最小化投影字段:在 EF Core、LINQ to SQL 中,仅选择所需字段以减少网络与内存开销。
  4. 表达式复用:将常用过滤、排序表达式封装为 Expression<Func<T, bool>> 以便重用。
  5. 谨慎使用 PLINQ:仅对 CPU 密集型、可无序分区的内存集合并行处理,避免在 I/O 密集型场景或需保序逻辑中使用。