>> >> >> Reference << << << <<<<<<Ref>>>>>>
LINQ
Modified: 2025-06-01 | Author:ljf12825

LINQ的全称是“Language-Integrated Query”,即语言继承查询
可以把它理解成C#(或 VB.NET)语言的一部分,它允许你使用类似SQL的语法,直接在代码中对各种数据源进行查询和操作

核心思想:用一种统一的方式来查询任何类型的数据,无论数据是存在于

LINQ存在的意义

在没有LINQ的时代,开发者面临一些问题

LINQ解决了这些问题

语法

LINQ提供了两种等价的语法:查询语法和方法语法

查询语法

这种语法看起来很想SQL,关键在包括from, where, select, orderby, group by, join等。它可读性更强,尤其是对于熟悉SQL的人
示例:从一个数字列表中筛选出大于10的数字,并排序

List<int> numbers = new List<int> { 5, 12, 8, 20, 3, 18 };

// 查询语法
var query = from num in numbers
            where num > 10
            orderby num descending
            select num;

foreach (var num in query)
{
    Console.WriteLine(num);
}
// 输出:20, 18, 12

方法语法(基于扩展方法)

这种语法依赖于扩展方法和Lambda表达式。它在System.Linq命名空间下定义了一系列扩展方法(如Where, Select, OrderBy等),可以直接在集合上调用

示例:同上

List<int> numbers = new List<int> { 5, 12, 8, 20, 3, 18 };

// 方法语法
var query = numbers
            .Where(num => num > 10)
            .OrderByDescending(num => num)
            .Select(num => num); // 这里的 Select 可以省略,因为就是选择自己

foreach (var num in query)
{
    Console.WriteLine(num);
}
// 输出: 20, 18, 12

两种风格的关系

核心概念与操作符

LINQ操作符可以分为两大类

示例

假设有一个Student

public class Student
{
    public string Name { get; set; }
    public int Age { get; set; }
    public int Score { get; set; }
}

和一组数据

List<Student> student = new List<Stuednt> {
    new Student { Name = "Alice", Age = 20, Score = 85 },
    new Student { Name = "Bob", Age = 22, Score = 90 },
    new Student { Name = "Charlie", Age = 19, Score = 78 },
    new Student { Name = "Diana", Age = 21, Score = 92 }
};
// 找到所有年龄大于20的学生
var result = students.Where(s => s.Age > 20);
// 只获取学生的名字列表
var names = students.Select(s => s.Name);
// 创建一个匿名类型
var info = students.Select(s => new { s.Name, s.Score });
// 按年龄分组
var groups = students.GroupBy(s => s.Age);
foreach (var group in groups)
{
    Console.WriteLine($"Age: {group.Key}");
    foreach (var student in group)
    {
        Console.WriteLine($" - {student.Name}");
    }
}
int count = students.Count(); // 学生总数
double avgScore = students.Average(s => s.Score); // 平均分
int maxScore = students.Max(s => s.Score); // 最高分
// 获取一个分数大于80的学生(如果没有,First会抛出异常,FirstOrDefault返回null)
var first = students.FirstOrDefault(s => s.Score > 80);
// 获取唯一一个叫“Bob”的学生(如果没有或超过一个,Single会抛异常)
var single = students.Single(s => s.Name == "Bob");

LINQ提供程序

LINQ的强大之处在于它的可扩展性。不同的数据源有不同的LINQ提供程序,它们负责将LINQ查询翻译成该数据源的原生查询语言

底层

LINQ的底层主要建立在三大核心技术之上

扩展方法 - 语法的基础

LINQ的方法语法完全依赖于扩展方法。这些方法定义在System.Linq命名空间中,它们扩展了IEnumerable<T>接口
示例:实现一个简单的Where方法来查看本质

// LINQ Where 操作符简化版
public static class MyLinqExtensions
{
    // 这是一个扩展方法
    public static IEnumerable<T> Where<T>(
        this IEnumerable<T> source,
        Func<T, bool> predicate // 关键:接受一个委托
    ) 
    {
        // 验证参数
        if (source == null) throw new ArgumentNullException(nameof(source));
        if (predicate == null) throw new ArgumentNullException(nameof(predicate));

        // 核心逻辑
        foreach (T item in source)
        {
            if (predicate(item)) // 对每个元素应用过滤条件
            {
                yield return item; // 使用 yield return 实现迭代器
            }
        }
    }
}

总结:接受一个数据源和委托,对这个数据源中的所有数据调用委托方法,对符合条件的数据使用yield return形成迭代器
关键点:

委托和Lambda表达式 - 行为参数化

LINQ操作符的核心是能够接收”行为“作为参数,这是通过委托实现的
委托的演进

// 传统委托(LINQ出现前的方式)
Func<int, bool> predicate1 = new Func<int, bool>(IsGreaterThanTen);

// 匿名方法(C# 2.0)
Func<int, bool> predicate2 = delegate(int x) { return x > 10; };

// Lambda表达式(C# 3.0, LINQ的伴侣)
Func<int, bool> predicate3 = x => x > 10;

static bool IsGreaterThanTen(int x) { return x > 10; }

在LINQ中的实际应用

var result = numbers.Where(x => x > 10);
// 编译器会将 x => x > 10 编译成:
// Func<int, bool> redicate = x => x > 10;
// numbers.Where(predicate);

延迟执行(Deferred Execution)

这是LINQ的最重要的特性之一,也是最容易产生困惑的地方

查询在定义时不会立即执行,只有在真正需要结果时(如遍历、调用ToList()等)才会执行

var numbers = new List<int> { 1, 2, 3, 4, 5 };

// 这只是定义查询,不会执行
var query = numbers.Where(x => {
    Console.WriteLine($"检查:{x}"); // 这行不会立即输出
    return x > 2;
});

Console.WriteLine("查询已定义");

// 现在才开始真正执行
foreach (var num in query) // 这里才会输出 ”检查:1“, ”检查:2“等
{
    Console.WriteLine($"结果:{num}");
}

立即执行方法

与延迟执行对应,有些方法会强制查询立即执行

// 这些方法会立即执行查询
var list = query.ToList();      // 转换为 List
var array = query.ToArray();    // 转换为 Array
var count = query.Count();      // 获取数量
var first = query.First();      // 获取第一个元素

表达式树和IQueryable - LINQ to SQL的魔法

这是LINQ最精妙的部分,也是区分IEnumerable<T>IQueryable<T>的关键

// 假设有一个数据库上下文
var dbContext = new MyDbContext();

// 这样写有问题
var badQuery = dbContext.Customers
                        .AsEnumerable() // 转换为 IEnumerable
                        .Where(c => c.City == "London");

// 实际执行过程:
// 1. SELECT * FROM Customers -> 把所有数据拉到内存中
// 2. 在内存中执行 .Where(c => c.City == "London")

IQueryable<T>的解决方案

// 正确的写法
var goodQuery = dbContext.Customers
                        .AsQueryable() // 实际上是IQueryable(通常默认就是)
                        .Where(c => c.City == "London");

// 实际执行过程
// 1. 构建表达式树
// 2. 最终执行:SELECT * FROM Customers WHERE City = 'London'

表达式树的本质

// Lambda 表达式给 IEnumerable:编译成委托
Func<Customer, bool> predicate = c => c.City == "London";

// Lambda 表达式给 IQueryable:编译成表达式树
Expression<Func<Customer, bool>> expression = c => c.City == "London";

// 表达式树是一个数据结构,可以分析、分解、翻译
// LINQ Provider (如 EF Core)会分析这个表达式树,生成对应的SQL

表达式树的简单分析

Expression<Func<Customer, bool>> expression = c => c.City == "London";

// 可以分析表达式树的结构
var body = expression.Body as BinaryExpression; // 这是一个二元表达式
var left = body.Left as MemberExpression; // 左边:c.City
var right = body.Right as ConstantExpression; // 右边:"London"

Console.WriteLine($"左边: {left.Member.Name}");    // 输出: City
Console.WriteLine($"操作: {body.NodeType}");       // 输出: Equal
Console.WriteLine($"右边: {right.Value}");          // 输出: London

IEnumerable<T>IQueryable<T>

核心区别:执行位置

IEnumerable<T>-客户端查询
特点
工作原理
List<Student> students = GetStudentsFromMemory();

// IEnumerable - 客户端执行
IEnumerable<Student> query = students
    .Where(s => s.Age > 20) // 在内存中过滤
    .OrderBy(s => s.Name) // 在内存中排序
    .Select(s => new { s.Name, s.Age }); // 在内存中投影

// 执行过程:
// 1. 从数据源获取所有数据
// 2. 在应用程序内存中执行所有查询操作
底层机制
// IEnumerable 的 Where 方法大致实现
public static IEnumerable<T> Where<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
    foreach (T item in source) // 在客户端循环
    {
        if (predicate(item)) // 在客户端执行过滤逻辑
            yield return item;
    }
}
IQueryable<T>-数据源查询
特点
工作原理
IQueryable<Student> students = dbContext.Students; // 来自数据库

// IQueryable - 数据源端执行
IQueryable<Student> query = students
    .Where(s => s.Age > 20)        // 生成 SQL WHERE 子句
    .OrderBy(s => s.Name)          // 生成 SQL ORDER BY 子句
    .Select(s => new { s.Name, s.Age }); // 生成 SQL SELECT 子句

// 执行过程:
// 1. 构建表达式树
// 2. 将表达式树翻译成数据源原生查询语言(如SQL)
// 3. 在数据源端执行查询
// 4. 只返回最终结果到客户端
底层机制
// IQueryable 使用表达式树
IQueryable<Student> query = students
    .Where(s => s.Age > 20); // 这里 s => s.Age > 20 是表达式树,不是委托

// 实际类型是:
Expression<Func<Student, bool>> expression = s => s.Age > 20;

LINQ Provider 的工作流程

以 Entity Framework 为例

  1. 构建表达式树:你的LINQ查询被转换成表达式树
  2. 分析表达式树:LINQ Provider 遍历表达式树,理解你的查询意图
  3. 生成SQL:根据表达式树生曾最优的SQL语句
  4. 执行查询:在数据库上执行SQL
  5. 物化结果:将数据库返回的数据转换成.NET对象

性能

LINQ的性能表现不能一概而论,它既可能非常高效,也可能成为性能瓶颈,完全取决于如何使用

LINQ to Objects

开销来源

  1. 委托调用开销
var result = list.Where(x => x > 10);
// 每个元素都需要通过委托调用Lambda表达式
// 这比直接for循环有额外开销
  1. 迭代器模式开销
// LINQ 使用 yield return, 每次 MoveNext()都有状态机开销
public static IEnumerable<T> Where<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
    foreach (T item in source) // 迭代器状态机
    {
        if (predicate(item))
            yield return item; // 状态保持和恢复
    }
}

LINQ to Objects 性能最佳实践

好的做法

  1. 合理使用立即执行方法
// 如果需要多次遍历,先物化
var frequentList = expensiveQuery.ToList();

// 多次使用
var count = frequentList.Count;
var first = frequentList.First();
  1. 选择合适的数据结构
// List 适合遍历,HashSet适合查找
var hashSet = new HashSet<int>(numbers);
bool exists = hashSet.Contains(123); // O(1)

// 比下面这个快得多
bool existsSlow = numbers.Any(x => x == 123); // O(n)
  1. 使用索引优化
// 对于 List,可以混合使用
var list = numbers.ToList();
for (int i = 0; i < list.Count; i++)
{
    if (someCondition(list[i]))
    {
        // 使用索引操作
    }
}

性能陷阱

  1. 重复计算
// 不好:重复执行相同查询
if (users.Any(u => u.Age > 65))
{
    var seniors = users.Where(u => u.Age > 65); // 重新执行!
    Process(seniors);
}

// 好的做法
var seniors = users.Where(u => u.Age > 65).ToList();
if (seniors.Any())
{
    Process(seniors);
}
  1. 在循环中使用LINQ
// 性能灾难:O(n^2)复杂度
foreach (var category in categories)
{
    var products = allProducts.Where(p => p.CategoryId == category.Id); // 每次都要扫描
    Process(products);
}

// 优化:使用 Lookup 或 GroupBy
var productsByCategory = allProducts.ToLookup(p => p.CategoryId);
foreach (var category in categories)
{
    var products = productsByCategory[category.Id]; // O(1) 查找
    Process(products);
}
  1. 不必要的排序
// 不好:只需要第一个,却排序了整个集合
var oldest = users.OrderByDescending(u => u.Age).First();

// 好的做法:O(n) vs O(n log n)
var oldest = users.MaxBy(u => u.Age); // .NET 6+
// 或者
var oldest = users.Aggregate((max, next) => next.Age > max.Age ? next : max);

LINQ to Entities(数据库LINQ)性能分析

性能可以很好的情况
当LINQ Provider能生成高效的SQL时

// 好的 LINQ - 生成高效的 SQL
var result = dbContext.Orders
    .Where(o => o.OrderDate >= startDate && o.OrderDate <= endDate)
    .Where(o => o.Status == "Completed")
    .Select(o => new { o.Id, o.TotalAmount })
    .ToList();

// 生成的 SQL 可能是:
// SELECT Id, TotalAmount FROM Orders 
// WHERE OrderDate BETWEEN @startDate AND @endDate AND Status = 'Completed'

性能灾难情况

  1. N+1查询问题(最常见)
// 性能灾难:N+1 查询
var orders = dbContext.Orders.Take(100).ToList();

foreach (var order in orders)  // 循环 100 次
{
    // 每次循环都执行一次数据库查询!
    var customer = dbContext.Customers.Find(order.CustomerId);
    Console.WriteLine($"{order.Id} - {customer.Name}");
}

// 总共执行:1 (取orders) + 100 (循环查询) = 101 次数据库往返

解决方案:使用Include或投影

// 方案1: 预先加载 (Eager Loading)
var orders = dbContext.Orders
    .Include(o => o.Customer)  // 一次性 JOIN 查询
    .Take(100)
    .ToList();

// 方案2: 投影 (Projection)
var orders = dbContext.Orders
    .Join(dbContext.Customers,
          o => o.CustomerId,
          c => c.Id,
          (o, c) => new { Order = o, CustomerName = c.Name })
    .Take(100)
    .ToList();
  1. 在客户端进行筛选
// 不好:把所有数据拉到内存中筛选
var users = dbContext.Users
    .AsEnumerable()  // 切换到客户端处理!危险!
    .Where(u => u.Age > 18)
    .ToList();

// 执行过程:
// 1. SELECT * FROM Users (可能返回百万行)
// 2. 在内存中执行 .Where(u => u.Age > 18)

正确做法:保持IQueryable

// 好:在数据库端筛选
var users = dbContext.Users
    .Where(u => u.Age > 18)  // 生成 WHERE 子句
    .ToList();
  1. 选择过多列
// 不好:选择不需要的列
var names = dbContext.Users
    .Select(u => u)  // SELECT * FROM Users
    .ToList()
    .Select(u => u.Name);  // 在内存中提取 Name

// 好:只选择需要的列
var names = dbContext.Users
    .Select(u => u.Name)  // SELECT Name FROM Users
    .ToList();

性能总结

场景性能表现建议
简单内存操作轻微开销,通常可接受优先考虑代码可读性
复杂内存操作可能有显著开销考虑传统循环或优化算法
数据库查询(正确使用)非常好,接近手动SQL保持 IQueryable,避免 N+1
数据库查询(错误使用)可能极差监控生成的 SQL,理解延迟加载
性能关键算法不适合使用传统循环和数组

最终建议