Effective-CSharp-38考虑用lambda表达式来代替方法
这条建议可能听起来有些奇怪,因为 lambda 表达式代替方法会重复编写代码。例如:[1]
1 | var allEmployees = FindAllEmployees(); |
你可以将这些 where 合并为一条子句,然而这并不会带来太大区别。查询操作之间本就可以拼接 (见31条),而简单的 where 谓词还会有可能内联 (inline),因此,这两种写法在性能上是一样的。[2]
看到刚才那段代码,你可能会想把重复的 lambda 表达式提取到方法,以便复用:[3]
1 | // Factor out method: |
如果将来需要修改员工的类别 (Classification),或修改筛选底薪员工时所依据的最低月薪 (MonthlySalary),那么只需要改动 LowPaidSalaried() 里的逻辑即可。
但是这样的重构不能提高其复用性,这与 lambda 表达式求值、解析及执行机制有关:[4]
-
LINQ to Objects
为执行表达式中代码,将 lambda 表达式转化成为委托
-
LINQ to SQL
根据 lambda 表达式创建表达式树,并解析,使其能在其他环境执行[5]
LINQ to Objects 针对本地数据存储 (local data store) 来执行查询,数据通常放在泛型集合。系统根据 lambda 表达式中的逻辑建立匿名委托,并执行相关代码。LINQ to Objects 扩展方法使用 IEnumerable
LINQ to SQL 使用表达式树,根据所写查询逻辑构建表达式树,将其解析为适当的 T-SQL 查询,这种查询是针对数据库执行的,LINQ to SQL 把 T-SQL 形式的查询字符串发送给数据库引擎并执行。这种处理方式要求 LINQ to SQL引擎必须解析表达式树,并把其中每一项操作都替换成等价的 SQL,这意味着所有的方法调用都需要换成 Expression.MethodCall 节点。然而 LINQ to SQL 引擎并不能把每一种方法调用都顺利的转化为 SQL 表达式,无法转换将会引发异常。[7]
如果所写的程序库需要支持任意类型的数据源,必须考虑上述情况。需要分开编写 lambda 表达式,且内联至代码中,以保证程序库正常运行。[8]
这并不是在鼓励一味的复制代码,而是提醒,涉及查询表达式及 lambda 的地方应该用更为合理的方法去创建复用代码块。例如:[9]
1 | private static IQueryable<Employee> LowPaidSalariedFilter(this IQueryable<Employee> sequence) => |
并不是每一种查询都能像这样简单的改写,可能需要沿着调用链寻找,才能发现可供复用的列表处理逻辑 (list-processing logic),从而提取相同的 lambda 表达式。31条提过,只要当程序真的需要遍历集合的时,enumerator 方法才会得以执行。可利用此特征,将查询操作分成许多小方法来写,这些小方法都能复用某一套 lambda 表达式来执行它所应该完成的那一部分查询工作。这些方法需要将待处理的序列当成输入值,并以 yield return 形式返回处理结果。这些小方法可构成较大的查询模块。避免重复的代码,使得代码结构更合理。[10]
也可按照同样的思路建立表达式树,以此拼接 IQueryable 形式的 enumerator,令查询操作能够远程执行。寻找相关员工所用的那棵表达式树可以先于其他查询拼接,然后执行。IQueryProvider 对象 (LINQ to SQL 引擎的数据源就是这种对象) 可以把全套查询操作一次执行完毕,而不必将其分解成多个部分放到本地执行。[11]
若想在复杂的查询中有效地复用 lambda 表达式,可以考虑针对封闭的泛型类型创建扩展方法。LowPaidSalariedFilter 方法就是这么写的,它接受有待筛选的 Employee 对象序列,输出经过筛选后的 Employee 对象序列。在实际工作中,还应该创建以 IEnumerable
你可以把查询操作分成多个小方法,其中一些方法在其内部用 lambda 表达式处理序列,另一些方法以 lambda 表达式作参数。把这些小方法拼接起来,以实现整套操作。这样既可以支持 IEnumerable
Bill Wagner, Effective C# (Covers C# 6.0), 3rd Edition 在本条目中讨论查询谓词复用;Microsoft Learn Lambda expressions and anonymous functions 说明 lambda 可以转换为委托或表达式树,因此“是否抽方法”取决于后续执行模型。 ↩︎
Effective C# Item 38 将多个
where子句和合并谓词视为等价查询结构的一种写法;这里的性能判断依赖具体 provider 和运行时优化,脚注只支撑“查询操作可组合、短谓词通常不应为了抽象而破坏查询形状”这一设计边界。 ↩︎Microsoft Learn Delegates 和 Local functions 支撑具名方法、局部函数适合表达可复用或有业务名的逻辑;后文的限制主要发生在
IQueryable<T>provider 需要翻译表达式树时。 ↩︎Microsoft Learn Lambda expressions and anonymous functions 说明 lambda 可转换为委托类型或表达式树类型;这正是 LINQ to Objects 与
IQueryable<T>provider 场景差异的基础。 ↩︎Microsoft Learn Delegates、
IQueryable<T>Interface 与 Expression trees 分别支撑“本地委托执行”和“表达式树交给 provider 分析/执行”的区别。 ↩︎Microsoft Learn
IEnumerable<T>Interface 表示可枚举序列;Lambda expressions 与 Delegates 支撑 LINQ to Objects 中谓词通常作为本地委托执行。 ↩︎Microsoft Learn
IQueryable<T>Interface 说明查询与 provider 关联,枚举会触发表达式树执行;Expression trees 说明表达式树表示代码结构,但具体 provider 不一定能翻译所有方法调用。 ↩︎Effective C# Item 38 与 Microsoft Learn
IQueryable<T>/ expression tree 文档共同支撑这一边界:面向任意 provider 的查询库应保留 provider 能看见的成员访问、比较和布尔组合结构,而不是只暴露一个普通 .NET 方法调用。 ↩︎Microsoft Learn
IQueryable<T>Interface 和 Expression trees 支撑把复用单位设计成仍返回IQueryable<T>的查询片段,让后续过滤继续留在同一表达式树中。 ↩︎对
IEnumerable<T>场景,Microsoft Learnyieldstatement 支撑用迭代器逐项返回处理结果;这与前一条序列 API 可组合性结论一致,即把列表处理逻辑拆成可拼接的小步骤。 ↩︎Microsoft Learn
IQueryable<T>Interface 与 Expression trees 支撑IQueryProvider通过表达式树执行查询;组合IQueryable<T>片段可以把整体查询结构保留给 provider。 ↩︎Microsoft Learn Extension members 支撑用扩展方法表达查询片段;分别提供
IEnumerable<T>与IQueryable<T>版本,可以区分本地委托执行和 provider 翻译两种语义。 ↩︎本文总结由 Effective C# Item 38、Microsoft Learn Lambda expressions、
IQueryable<T>、Expression trees 与 Extension members 共同支撑:查询逻辑可以拆成小方法,但应保留IEnumerable<T>与IQueryable<T>各自需要的执行形状。 ↩︎