×

别再这样写LINQ了:5个LINQ性能优化技巧

独孤求败 独孤求败 发表于2026-03-18 14:26:24 浏览31 评论0

抢沙发发表评论

很多人觉得 LINQ 慢,其实慢的不是 LINQ 本身,而是我们没完全理解它的执行方式。LINQ 写起来很顺手,但如果不清楚背后的延迟执行(deferred execution)和内存分配机制,性能就容易打折扣。下面这五个技巧,能帮你在高频使用的代码里,把 LINQ 的效率提上去。


技巧一:别让同一个查询执行多次

LINQ 查询定义的时候并不会真的去跑数据,它只是返回一个 IEnumerable<T>,等到你用 foreach 去遍历,或者调用 Count()FirstOrDefault() 这些方法时,它才会真正执行。如果同一个查询被反复使用,那背后的数据源就会被反复访问,造成不必要的开销。

var query = users.Where(u => u.IsActive).Select(u => new UserDto(u.Id, u.Name));
Console.WriteLine(query.Count());        // 第一次执行
var first = query.FirstOrDefault();       // 第二次执行
foreach (var user in query) { ... }       // 第三次执行

正确的做法:如果数据需要多次使用,就用 ToList() 或 ToArray() 把查询结果缓存下来,这样数据只加载一次。

var cached = users.Where(u => u.IsActive)
                  .Select(u => new UserDto(u.Id, u.Name))
                  .ToList(); // 只执行一次,结果存到列表里

技巧二:留意内存分配

LINQ 的方法链(比如 WhereSelect)内部会创建枚举器(enumerator),而大多数枚举器是引用类型,会在堆上分配内存。如果在游戏循环或高频数据处理中频繁调用,这些临时分配的内存会给垃圾回收(GC)带来压力。

优化思路:如果只是简单的遍历和过滤,可以考虑用 Span<T> 配合手动循环,实现零内存分配:

Span<int> span = numbers.AsSpan();
var results = new List<int>(capacity: 10);
foreach (ref readonly var n in span)
{
    if ((n & 1) == 0// 判断偶数
    {
        results.Add(n);
        if (results.Count == 10break// 找到10个就提前退出
    }
}

这种方式绕过了 LINQ 的枚举器,在性能敏感的代码路径里特别有用①。


技巧三:先过滤,再投影

写 LINQ 时如果顺序不对,会白白浪费资源。比如先把所有对象转换成 DTO,再过滤,意味着很多对象白转了:

// 不推荐:先投影再过滤
var dtos = products.Select(p => new ProductDto(p.Id, p.Price))
                   .Where(dto => dto.Price > 500)
                   .ToList();

正确的顺序:先用原始对象过滤掉不需要的数据,再投影成 DTO:

// 推荐:先过滤再投影
var dtos = products.Where(p => p.Price > 500)
                   .Select(p => new ProductDto(p.Id, p.Price))
                   .ToList();

在 EF Core 里,这个顺序还能让生成的 SQL 查询只返回需要的列,减少网络传输和对象映射的开销②。


技巧四:利用短路方法提前退出

有人习惯写成 Where(...).Take(1).FirstOrDefault(),这其实多了一步操作。FirstOrDefault(predicate) 本身就支持条件判断,并且会在找到第一个匹配项后立即停止,不需要构建中间集合:

// 不推荐:多了一层包装
var user = users.Where(u => u.Email == email).Take(1).FirstOrDefault();

// 推荐:直接短路退出
var user = users.FirstOrDefault(u => u.Email == email);

// 如果只是想判断是否存在
bool exists = users.Any(u => u.Email == email);

像 AnyFirstOrDefaultSingleOrDefault 这些方法都支持直接传条件,应该优先用它们来实现即时退出③。


技巧五:有意识地选择物化方式

ToList().Count 是个常见的低效写法——它会先把所有数据加载到内存,然后再计数。直接调用 Count(predicate) 会更高效:

// 不推荐:先物化再计数
var count = users.Where(u => u.IsActive).ToList().Count;

// 推荐:直接聚合
var count = users.Count(u => u.IsActive);

根据后续操作的不同,选择合适的物化方式也很重要:

  • 需要随机访问?用 ToArray()
  • 需要按键查找?用 ToDictionary()
  • 需要分组访问?用 ToLookup()

尽量避免“先转列表再转字典”这种冗余步骤,一步到位最干净②。


性能源于对内存模型的理解

不同的 LINQ 数据源,执行方式也不一样:

  • IEnumerable<T>:延迟执行,基于堆分配的迭代器;
  • IQueryable<T>:转换为表达式树,由 EF Core 这类提供程序翻译成 SQL;
  • Span<T>:栈上的只读结构,零分配,适合局部高性能计算。

真正的性能优化,不是彻底抛弃 LINQ,而是在合适的场景选择合适的抽象,带着对底层机制的“同理心”去写代码④。


给进阶开发者的 LINQ 使用原则

  • 绝不重复枚举:物化一次,复用多次;
  • 尽早过滤:减少后续处理的数据量;
  • 延迟投影:只在需要的时候创建新对象;
  • 积极短路:利用 AnyFirstOrDefault 等方法提前退出;
  • 有意物化:根据实际需求选择 CountToDictionary 等终端操作。

遵循这些原则,LINQ 既能保持代码的简洁优雅,也能在性能要求高的场景下接近手写循环的效率。


群贤毕至

访客