Effective-CSharp-38考虑用lambda表达式来代替方法
这条建议可能听起来有些奇怪,因为 lambda 表达式代替方法会重复编写代码。例如:
1 | var allEmployees = FindAllEmployees(); |
你可以将这些 where 合并为一条子句,然而这并不会带来太大区别。查询操作之间本就可以拼接 (见31条),而简单的 where 谓词还会有可能内联 (inline),因此,这两种写法在性能上是一样的。
看到刚才那段代码,你可能会想把重复的 lambda 表达式提取到方法,以便复用:
1 | // Factor out method: |
如果将来需要修改员工的类别 (Classification),或修改筛选底薪员工时所依据的最低月薪 (MonthlySalary),那么只需要改动 LowPaidSalaried() 里的逻辑即可。
但是这样的重构不能提高其复用性,这与 lambda 表达式求值、解析及执行机制有关:
LINQ to Objects
为执行表达式中代码,将 lambda 表达式转化成为委托
LINQ to SQL
根据 lambda 表达式创建表达式树,并解析,使其能在其他环境执行
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 表达式,无法转换将会引发异常。
如果所写的程序库需要支持任意类型的数据源,必须考虑上述情况。需要分开编写 lambda 表达式,且内联至代码中,以保证程序库正常运行。
这并不是在鼓励一味的复制代码,而是提醒,涉及查询表达式及 lambda 的地方应该用更为合理的方法去创建复用代码块。例如:
1 | private static IQueryable<Employee> LowPaidSalariedFilter(this IQueryable<Employee> sequence) => |
并不是每一种查询都能像这样简单的改写,可能需要沿着调用链寻找,才能发现可供复用的列表处理逻辑 (list-processing logic),从而提取相同的 lambda 表达式。31条提过,只要当程序真的需要遍历集合的时,enumerator 方法才会得以执行。可利用此特征,将查询操作分成许多小方法来写,这些小方法都能复用某一套 lambda 表达式来执行它所应该完成的那一部分查询工作。这些方法需要将待处理的序列当成输入值,并以 yield return 形式返回处理结果。这些小方法可构成较大的查询模块。避免重复的代码,使得代码结构更合理。
也可按照同样的思路建立表达式树,以此拼接 IQueryable 形式的 enumerator,令查询操作能够远程执行。寻找相关员工所用的那棵表达式树可以先于其他查询拼接,然后执行。IQueryProvider 对象 (LINQ to SQL 引擎的数据源就是这种对象) 可以把全套查询操作一次执行完毕,而不必将其分解成多个部分放到本地执行。
若想在复杂的查询中有效地复用 lambda 表达式,可以考虑针对封闭的泛型类型创建扩展方法。LowPaidSalariedFilter 方法就是这么写的,它接受有待筛选的 Employee 对象序列,输出经过筛选后的 Employee 对象序列。在实际工作中,还应该创建以 IEnumerable
你可以把查询操作分成多个小方法,其中一些方法在其内部用 lambda 表达式处理序列,另一些方法以 lambda 表达式作参数。把这些小方法拼接起来,以实现整套操作。这样既可以支持 IEnumerable