Chapter 4: Working with LINQ (Items 29-44)

完整覆盖第3版Item 29-44:iterator、query composition、function parameter、extension resolution、lazy/deferred execution、capture、IEnumerable/IQueryable与cardinality。

学习目标

  • 能设计iterator与composable sequence API,区分query construction、enumeration和materialization的成本与ownership
  • 能推导query syntax到method calls、delegate到expression tree及extension resolution,判断实际执行位置与失败时机
  • 能分析captured resource、IEnumerable/IQueryable、cardinality和bound variable,复现重复执行与provider差异

机制总览

LINQ 查询从表达式到执行边界

  1. 1

    组合查询

    Where、Select 等操作先组成可复用计划,不应偷偷执行或产生副作用。

  2. 2

    选择 Provider

    IEnumerable 执行 CLR 委托,IQueryable 把表达式交给远端 provider 翻译。

  3. 3

    触发枚举

    ToList、First、Single 或 foreach 明确执行时刻和结果基数。

先按顺序建立机制,再进入实验切换阶段并检查失效证据。

章级决策实验

LINQ 查询从表达式到执行边界

切换查询阶段,判断代码是在组合计划、枚举本地序列,还是翻译远端表达式。

选择推理阶段

当前阶段 · 组合查询

Where、Select 等操作先组成可复用计划,不应偷偷执行或产生副作用。

可核验证据

零枚举断言、表达式树与副作用计数。

LINQ 的可靠性取决于清楚标出 provider、枚举次数、资源寿命和 cardinality;语法外观不能代替执行模型。

失效—证据矩阵

LINQ 查询从表达式到执行边界

组合查询

典型失效

组合函数抛异常或修改外部状态,使同一查询不可重放。

核验证据

零枚举断言、表达式树与副作用计数。

选择 Provider

典型失效

把本地方法放入远端表达式,翻译失败或退化为大规模客户端计算。

核验证据

生成 SQL、provider 日志与数据传输量。

触发枚举

典型失效

重复枚举、资源已释放或 Single/First 语义选错。

核验证据

查询次数、连接寿命与 0/1/多条数据测试。

每个判断都必须能落到观测、测试或产物,不能只凭代码表面推测。

为什么LINQ问题总在Enumeration时暴露

WhereSelect看似已经“得到结果”,多数时候只是创建可枚举pipeline。真正的I/O、callback、exception、resource use和closure read发生在terminal operation或foreach。因此设计LINQ API必须把construction、execution、materialization三种时刻写成contract。

先预测:返回IEnumerable<T>是否意味着已经有collection;把loop改成query syntax是否一定只执行一次;同一lambda用于IEnumerableIQueryable是否必然同语义?答案都是否。

Sequence Production与Composition(Items 29-33)

Item 29: Prefer Iterator Methods to Returning Collections

当consumer可能只取前几项、source很大或可流式生成时,iterator用yield return避免先构建完整collection。若API承诺stable snapshot、随机访问或单次I/O结果,则明确materialize并返回read-only collection;不要仅为“看起来LINQ”改变语义。

Item 30: Prefer Query Syntax to Loops

Query syntax适合join、group、multiple from等关系表达,method syntax适合短pipeline和只存在于method form的operators。选择依据是意图是否清楚,不是统一风格;两者最终映射到method calls,performance由source/provider与operators决定。

Item 31: Create Composable APIs for Sequences

返回IEnumerable<T>/IQueryable<T>让caller继续filter、project、order,而不是在helper里固定ToList、print或mutation。Composition boundary仍应定义enumeration count、cancellation、ordering和resource lifetime,不能把所有责任推给caller。

Item 32: Decouple Iterations from Actions, Predicates, and Functions

把traversal与predicate/action/function分开,复用同一iteration policy并便于测试。Callback应尽量pure;若有side effect或fault,method name和contract必须说明短路、ordering、exception传播及是否可能重复调用。

Item 33: Generate Sequence Items as Requested

On-demand generation支持大序列或无限序列,让consumer用Take控制工作量。每次enumeration通常重新生成;涉及random/time/external data时,需决定是live semantics还是先capture seed/snapshot。

public static IEnumerable<int> Fibonacci()
{
    var (left, right) = (0, 1);
    while (true)
    {
        yield return left;
        (left, right) = (right, checked(left + right));
    }
}
分步1 / 3

切换iterator、query、composable API与generation

Query Binding与Evaluation Policy(Items 34-40)

Item 34: Loosen Coupling by Using Function Parameters

把policy作为Func/Predicate参数,让sequence algorithm不依赖具体业务class。Function parameter是显式dependency,便于测试不同策略;若参数数量膨胀或彼此共享state,则提升为named policy object而非继续堆lambda。

Item 35: Never Overload Extension Methods

原书强调避免让同名extension因static type、namespace或type inference选中意外版本。现代实践仍应让语义不同的operations使用不同名称,尤其不能让IEnumerable<T>IQueryable<T>同名自定义extension暗中改变执行位置。

Item 36: Understand How Query Expressions Map to Method Calls

from x in source where p select f映射到source.Where(...).Select(...);source的可用members决定绑定到Enumerable还是Queryable。理解mapping才能判断lambda被编译成delegate还是expression tree,以及provider能否翻译。

Item 37: Prefer Lazy Evaluation to Eager Evaluation in Queries

Lazy query可组合、可短路、避免不需要的work,并能看到enumeration时的最新source。若需要consistent snapshot、关闭resource、避免重复remote call或跨thread transfer,则在明确boundary ToList/ToArray,不是盲目延迟。

Item 38: Prefer Lambda Expressions to Methods

短、局部、单用途behavior用lambda贴近query;复杂branch、复用policy、需要独立文档/测试或容易抛异常时用named method。Lambda不是“更函数式”的保证,capture与side effect仍需审计。

Item 39: Avoid Throwing Exceptions in Functions and Actions

在per-element callback里用exception表示预期invalid data,会让整个pipeline在远离construction处中断。预期分支用filter、Try/Result或validation projection;真正broken invariant/dependency failure仍应抛出并由boundary处理,不能吞掉。

Item 40: Distinguish Early from Deferred Execution

Where/Select通常deferred;ToListCountFirst等terminal operator触发执行。一次Count()后再foreach可能执行两次;先决定需要live query还是snapshot,再把materialization放在ownership和consistency boundary。

var query = orders.Where(order => order.Total > minimum); // construction
var snapshot = query.ToList();                            // execution now
分步1 / 3

切换function、extension、mapping、lazy与terminal场景

Resource、Provider、Cardinality与Closure(Items 41-44)

Item 41: Avoid Capturing Expensive Resources

Iterator或lambda若capture stream、DbContext、large graph,会把lifetime延长到enumeration甚至publisher lifetime。要么iterator内部拥有并using到enumeration结束,要么在resource scope内materialize;不能返回一个依赖已Dispose context的deferred query。

Item 42: Distinguish between IEnumerable and IQueryable Data Sources

IEnumerable<T>在process内执行compiled delegates;IQueryable<T>构建expression tree交给provider翻译。Provider只支持有限nodes,performance取决于generated command、indexes、round trips与materialization point。测试必须检查translation和command,不只检查in-memory结果。

Item 43: Use Single() and First() to Enforce Semantic Expectations on Queries

Single断言exactly one,0或many都fault;First断言at least one且需要明确ordering;SingleOrDefault/FirstOrDefault还要处理default ambiguity。选择terminal operator就是选择cardinality contract,不能用First掩盖duplicate invariant violation。

Item 44: Avoid Modifying Bound Variables

Lambda capture的是variable storage,不是每次看到的value snapshot;deferred execution或循环注册callback时,之后修改会改变结果。用loop-local copy、immutable input或parameter传值,并避免多个queries共享可变threshold造成time-dependent behavior。

var predicates = new List<Func<int, bool>>();
foreach (var limit in new[] { 10, 20, 30 })
{
    var captured = limit;
    predicates.Add(value => value > captured);
}
分步1 / 3

切换resource、provider、cardinality与bound variable

本章回顾:Composition结束于明确的Execution Boundary

  1. Iterator与on-demand generation让consumer控制工作量,但也把resource lifetime延伸到enumeration。
  2. Query syntax映射到method calls;source static type决定delegate或expression-tree binding。
  3. Function parameter和extension可降低耦合,但必须保留resolution、fault与side-effect contract。
  4. Lazy与eager不是优劣标签;materialization boundary同时决定cost、snapshot与resource release。
  5. IQueryable需要translation/command evidence,不能用in-memory test替代。
  6. Single/First表达cardinality,capture读取execution-time storage;两者都必须测试边界值。

练习

问题 1:一个方法从DbContext返回IEnumerable,caller稍后枚举时报disposed,应如何修复?

问题 2:同一filter在List与EF provider上结果一致,是否足以证明正确?

问题 3:Count后foreach、First代替Single、循环capture分别隐藏了什么contract?

术语表

名词解释

本章出现的专业名词,用大白话再讲一遍。

deferred sequence
materialization boundary
query binding
capture lifetime
cardinality contract

讨论

评论区加载中…