很多人觉得 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 的方法链(比如 Where、Select)内部会创建枚举器(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 == 10) break; // 找到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);
像 Any、FirstOrDefault、SingleOrDefault 这些方法都支持直接传条件,应该优先用它们来实现即时退出③。
技巧五:有意识地选择物化方式
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 使用原则
绝不重复枚举:物化一次,复用多次; 尽早过滤:减少后续处理的数据量; 延迟投影:只在需要的时候创建新对象; 积极短路:利用 Any、FirstOrDefault等方法提前退出;有意物化:根据实际需求选择 Count、ToDictionary等终端操作。
遵循这些原则,LINQ 既能保持代码的简洁优雅,也能在性能要求高的场景下接近手写循环的效率。