惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

GbyAI
GbyAI
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
P
Proofpoint News Feed
L
Lohrmann on Cybersecurity
S
Secure Thoughts
Attack and Defense Labs
Attack and Defense Labs
人人都是产品经理
人人都是产品经理
Stack Overflow Blog
Stack Overflow Blog
W
WeLiveSecurity
O
OpenAI News
SecWiki News
SecWiki News
博客园 - Franky
NISL@THU
NISL@THU
Microsoft Azure Blog
Microsoft Azure Blog
T
Tor Project blog
Microsoft Security Blog
Microsoft Security Blog
aimingoo的专栏
aimingoo的专栏
Security Latest
Security Latest
H
Hacker News: Front Page
Google Online Security Blog
Google Online Security Blog
P
Privacy & Cybersecurity Law Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
D
Darknet – Hacking Tools, Hacker News & Cyber Security
月光博客
月光博客
李成银的技术随笔
Spread Privacy
Spread Privacy
F
Full Disclosure
F
Fortinet All Blogs
T
The Exploit Database - CXSecurity.com
Vercel News
Vercel News
AWS News Blog
AWS News Blog
WordPress大学
WordPress大学
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
V
Visual Studio Blog
J
Java Code Geeks
博客园 - 三生石上(FineUI控件)
G
Google Developers Blog
云风的 BLOG
云风的 BLOG
博客园 - 司徒正美
Engineering at Meta
Engineering at Meta
Last Week in AI
Last Week in AI
P
Palo Alto Networks Blog
宝玉的分享
宝玉的分享
T
True Tiger Recordings
N
News and Events Feed by Topic
酷 壳 – CoolShell
酷 壳 – CoolShell
Cisco Talos Blog
Cisco Talos Blog
N
News | PayPal Newsroom
S
SegmentFault 最新的问题
Jina AI
Jina AI

博客园 - 世纪末の魔术师

坚毅,是一种缓慢修理自己的方式 语言的边界,与软件的命运 人类与AI协同进化 《坚毅》第一部分读书笔记 用 System.CommandLine 构建工程级 CLI 工具 用 Command 模式构建可扩展的命令行工具 从哎呦”到语言宇宙 ——读《What Is ChatGPT Doing … And Why Does It Work?》 ⏱️ 深入理解定时器中的【时间轮算法】 🚫 为什么「定时器」不应该是线程安全的? C# AOT编译后——调用其类库方法因顺序出错? UnitTask中的Forget()与 CTS 光线追踪和球体追踪 八、方法(method) 二十、异常与状态管理(Exception&State Management) 二十八、IO绑定的异步操作(IO-Bound Async) 二十二、CLR寄宿与AppDomain(CLR Hosting and App Domains ) 二十九、原始线程同步构造(Primitive Thread Synchronization Constructs ) 二十六、线程与并发(Thread Basic) 二十七、计算密集型异步操作(Compute-Bound Asynchronous Operations) 二十三、程序集加载与反射(Assembly Loading and Reflection)
Linq延迟执行陷阱
世纪末の魔术师 · 2026-05-21 · via 博客园 - 世纪末の魔术师

Linq延迟执行陷阱

LINQ 延迟执行的核心机制

  • 延迟执行:定义查询时(如 WhereSelect不立即执行,只有在迭代(foreachToListCountFirst 等)时才会真正遍历数据源并执行逻辑。
  • 每次迭代独立执行:对同一个查询变量多次迭代,会重复执行底层的数据访问或计算。

陷阱1:多次枚举(重复执行)

var numbers = new List<int> { 1, 2, 3, 4, 5 };
var evenQuery = numbers.Where(n => n % 2 == 0);

int count = evenQuery.Count();   // 遍历一次,筛选
int first = evenQuery.First();   // 又遍历一次,直到找到第一个偶数

潜在问题

  • 若数据源是数据库(EF Core),每次枚举都会发送新的 SQL 查询,性能极差。
  • 若数据源是耗时计算(如读取大文件、远程 API),会重复执行昂贵的 I/O。
  • 若数据源在两次枚举之间发生变化(如集合元素被修改),会导致结果不一致。

追问应对

  • 使用 .ToList().ToArray() 将结果物化,后续操作基于内存副本。
  • 但注意:过早 ToList() 会失去延迟执行的优势(如数据库分页过滤),需权衡。
var evenList = evenQuery.ToList();   // 只执行一次
int count = evenList.Count;          // 直接取属性,不重复遍历
int first = evenList[0];

陷阱2:闭包捕获变量(变量被修改)

var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
    actions.Add(() => Console.Write(i));   // 捕获同一个变量 i
}
foreach (var a in actions) a();   // 输出 "333",而非 "012"

原因:Lambda 表达式中捕获的是变量本身,而不是创建时的值。for 循环中只有一个 i 变量,循环结束后 i 的值为 3。

追问变种

  • 若使用 foreach 呢?在 C# 5.0 之前,foreach 也存在同样问题;C# 5.0+ 中 foreach 循环变量在每次迭代中会被复制(类似新变量),因此不会出错。
  • 解决方式:在循环体内创建临时副本。
for (int i = 0; i < 3; i++)
{
    int temp = i;               // 每次迭代创建一个新变量
    actions.Add(() => Console.Write(temp));
}
// 输出 "012"

在 LINQ 中的体现

var items = new List<int> { 1, 2, 3 };
var query = items.Select(x => x * multiplier); // multiplier 变化会影响结果

陷阱3:数据源生命周期已结束(延迟执行时机错误)

IEnumerable<User> query;
using (var db = new MyDbContext())
{
    query = db.Users.Where(u => u.Age > 18);   // 未执行,只是定义查询
}   // 此处释放数据库连接

var result = query.ToList();   // ❌ 枚举时连接已关闭 → ObjectDisposedException

原因Where 返回的 IQueryable<T>IEnumerable<T> 依赖于 DbContext 实例。using 块结束后,上下文被销毁,但查询尚未执行。等到 ToList() 枚举时才尝试访问已释放的数据库连接。

正确做法

  • using 块内部立即物化结果:.ToList()
  • 或者保持 DbContext 存活直到枚举完成(例如注入到上层生命周期中)。

类似场景

  • 文件流读取:File.ReadLines(path) 返回延迟执行序列,若在 using 外枚举会抛出异常。
  • 自定义迭代器(yield return)中依赖的资源。

陷阱4:查询被意外多次迭代(常见于日志、调试)

var expensiveQuery = GetDataFromApi().Where(...);  // 延迟执行

Log($"Count = {expensiveQuery.Count()}");   // 执行1次
foreach (var item in expensiveQuery)       // 执行2次(又请求API)
{
    Process(item);
}

后果:在调试时,鼠标悬停展开查询变量可能会触发迭代(取决于调试器实现),导致意想不到的数据获取。

建议

  • 对昂贵或单次使用的查询,尽早 ToList()
  • 使用 System.Linq.Enumerable 中的扩展方法时,注意它们是否会触发迭代(CountAnyFirst 都会)。

陷阱5:IQueryable 与 IEnumerable 的混淆

IQueryable<User> dbQuery = db.Users.Where(u => u.Age > 18);
IEnumerable<User> enumerable = dbQuery;   // 隐式转换

// 后续使用 Where 扩展方法:使用的是 Enumerable.Where 还是 Queryable.Where?
var result = enumerable.Where(u => u.Name.StartsWith("A")).ToList();
  • IQueryableWhere 会构建表达式树,最终在数据库端执行过滤。
  • 一旦赋值给 IEnumerable,后续的 LINQ 操作符(如 Where)将使用 Enumerable 扩展方法,数据会从数据库全部加载到内存,然后在内存中执行过滤,性能灾难

正确做法:保持 IQueryable 类型,直到所有查询条件都拼接完成,最后再 .ToList()


面试追问常见扩展

  1. 如何避免重复枚举?

    • 使用 .ToList().ToArray() 缓存结果。
    • 若只需要一次迭代,直接用 foreach 处理,不保留查询变量。
  2. 延迟执行的好处有哪些?

    • 组合多个操作形成表达式树,最终生成高效的单次查询(数据库、LINQ to XML)。
    • 避免执行不必要的数据转换(如 Select 只在实际需要时计算)。
    • 处理无限序列(如 Fibonacci().Take(10))。
  3. 哪些 LINQ 方法会立即执行?

    • 聚合:CountSumAverageMaxMinAggregate
    • 元素访问:FirstLastSingleElementAt
    • 转换:ToListToArrayToDictionaryToLookup
    • 布尔判断:AnyAllContains
  4. 如何判断一个方法是延迟还是立即执行?

    • 延迟执行的方法返回 IEnumerable<T>IOrderedEnumerable<T>接口/类型
    • 立即执行的方法返回具体的值(intboolTList<T> 等)。

总结:安全使用 LINQ 延迟执行的检查清单

  • ✅ 对同一个查询,不要迭代多次;如需多次,物化为集合。
  • ✅ 闭包中捕获循环变量时,使用临时副本。
  • ✅ 确保查询的枚举时机在数据源生命周期之内。
  • ✅ 明确区分 IQueryableIEnumerable,避免无意中切换到内存模式。
  • ✅ 理解方法的执行时机,避免在调试或日志中意外触发迭代。