查看原文
其他

[译]如何在C#中调试LINQ查询

The following article is from DotNetCore学习站 Author 芝麻麻雀

LINQ是我在C#中最喜欢的功能之一。它让代码看起来更漂亮美观。我们得到了一个易于编写和理解的简洁函数式语法。好吧,至少我们可以使用LINQ方法的语法风格。

LINQ很难进行调试。我们无法知道该查询内部发生了什么。我们可以看到输入和输出,但这就是它的全部。出现问题时会发生什么?我们只是盯着代码,试图获得某种洞察力?必须有一个更好的方式……

调试LINQ

虽然很难,但可以使用一些技术来调试LINQ。

首先,我们创建一个小场景。假设我们想要一份按年龄排序的3名男性员工的名单,这些员工的薪水高于平均水平。这是一个非常常见的查询类型,对吧?这是我为此编写的代码:

  1. public IEnumerable<Employee> MyQuery(List<Employee> employees)

  2. {

  3. var avgSalary = employees.Select(e=>e.Salary).Average();


  4. return employees

  5. .Where(e => e.Gender == "Male")

  6. .Take(3)

  7. .Where(e => e.Salary > avgSalary)

  8. .OrderBy(e => e.Age);

  9. }

数据集为:

姓名年龄性别收入
Peter Claus40“Male”61000
Jose Mond35"male"62000
Helen Gant38"Female"38000
Jo Parker42"Male"52000
Alex Mueller22"Male"39000
Abbi Black53"female"56000
Mike Mockson51"Male"82000

当运行此查询时,我得到的结果为:PeterClaus,61000,40

这似乎不对…… 应改有3名员工的。而平均工资约为56400,因此结果中应包括薪水为62000的“Jose Mond”和薪水为82000的“Mike Mockson”。

所以,我的LINQ查询中有一个错我,该怎么办呢?好吧,我可以盯着代码,直到我弄明白,这甚至可能适用于这种特殊情况。或者,我可以以某种方式调试它。让我们看看如何调试它。

1. 在快速监视中评估查询的各个部分

你可以做的最简单的事情之一就是在快速监视中分析各个查询。你可以从第一个操作开始,然后继续第一个和第二个操作,以此类推。

这里有一个例子:

你可以使用OzCode的显示功能来显示你感兴趣的字段,这样可以轻松找到问题。

我们可以看到即使在第一次查询之后,就出现了问题。“Jose Mond” 一个男性,貌似没有查询到。现在,我可以盯着一小段代码找出错误。我想我明白了,Jose的性别写成了“male”,而不是“Male”。我现在可以对查询做一个小的修复:

  1. var res = employees

  2. .Where(e => e.Gender.ToLower() == "male") // added "ToLower()"

  3. .Take(3)

  4. .Where(e => e.Salary > avgSalary)

  5. .OrderBy(e => e.Age);

修复后,执行代码得到结果为:

  1. Jose Mond, 62000, 35

  2. Peter Claus, 61000, 40

现在包括了Jose,所以修复了第一个错误。还有另一个错误,“Mike Mockson”仍然缺失,我们将用下一个技术解决。这种技术有其缺点。如果你需要在大集合中查找特定项目,则可能需要在快速监视窗口中话费大量时间。

另请注意,某些查询可以更改应用程序状态。例如,你可以在lambda函数中调用一个可以改变瞬时值的方法,像 varres=source.Select(x=>x.Age++) 。通过在快速监视窗口运行,将改变应用程序状态并危及调试会话。通过在表达式中添加 ,nse 无副作用后缀(no-side-effects postfix )避免这种情况。要使用它,首先将表达式复制到剪贴板,打开一个空的快速监视窗口,然后使用 ,nse后缀手动粘贴表达式。

2. 将断点放入lambda表达式中

另一个调试LINQ的好方法是在lambda表达式中放置一个断点。这允许评估单个项目。对应大型集合,你可以将其与条件断点功能结合使用。在我们的例子中,我们发现“Mike Mockson”不是第一个Where操作结果的一部分。你可以在 .Where(e=>e.Gender=="Male")lambda表达式中放置条件断点,条件为:e.Name=="Mike Mockson

运行查询后,我们将看到:

只打印了3个名字,那是因为我们的查询条件中有 .Take(3),在前3次匹配后停止评估。我们确实想要一份按年龄排序的3名男性员工的名单,这些员工薪水高于平均水平。所以我们可能应该在检查薪水后才使用 Take运算符。将查询改为一下内容:

  1. var res = employees

  2. .Where(e => e.Gender.ToLower() == "male")

  3. .Where(e => e.Salary > avgSalary)

  4. .Take(3)

  5. .OrderBy(e => e.Age);

正确的结果是:Jose MondPeter ClausMike Mockson

在LINQ to SQL中,这种技术不起作用。

3. 使用日志中间件方法

让我们回到错误尚未修复的初始状态,面对看似正确的查询,我们都傻眼了。

调试查询的另一个方法是使用以下扩展方法:

  1. public static IEnumerable<T> LogLINQ<T>(this IEnumerable<T> enumerable, string logName, Func<T, string> printMethod)

  2. {

  3. #if DEBUG

  4. int count = 0;

  5. foreach (var item in enumerable)

  6. {

  7. if (printMethod != null)

  8. {

  9. Debug.WriteLine($"{logName}|item {count} = {printMethod(item)}");

  10. }

  11. count++;

  12. yield return item;

  13. }

  14. Debug.WriteLine($"{logName}|count = {count}");

  15. #else

  16. return enumerable;

  17. #endif

  18. }

以下是如何使用它:

  1. var res = employees

  2. .LogLINQ("source", e=>e.Name)

  3. .Where(e => e.Gender == "Male")

  4. .LogLINQ("logWhere", e=>e.Name)

  5. .Take(3)

  6. .LogLINQ("logTake", e=>e.Name)

  7. .Where(e => e.Salary > avgSalary)

  8. .LogLINQ("logWhere2", e=>e.Name)

  9. .OrderBy(e => e.Age);

输出为:

说明和解释:

  • 在LINQ查询中的每个操作之后放置 LogLINQ方法。它可以选择打印通过此操作的所有项目和总数。

  • logName是每个输出的前缀,可以轻松查看编写它的查询步骤。我喜欢将其命名为之后操作相同的名称。

  • Fun<T,string>printMethod允许打印给定项目的任何内容。在上面的示例中,我选择使用 e=>e.Name打印员工的姓名,当为 null时,除总数外,不会打印任何内容。

  • 为了优化,此方法尽在调试模式下有效( #if DEBUG)。在发布模式下,它什么都不做。

  • 每个项目都按顺序打印,无需等待操作结束,这是因为LINQ的 lazy 特性。以下是查看单个操作结果的提示:将整个输出复制到 notepad++。然后使用Ctrl+Shift+F(Find)并查找日志前缀(例如 logWhere2)。在查找对话框,点击Find All in Current Document。这将仅显示与日志名称前缀匹配的行。

查看输出窗口,可以看到以下几点:

  1. 源中包括“Jose Mond”,但 logWhere没有,这是因为我们之前看到的区分大小写的错误。

  2. 由于提前使用 Take方法,“Mike Mockson”从未在源中进行评估。事实上,源的计数日志完全丢失,因为它永远不会到达集合的末尾。

对应 LINQ to SQL以及可能的其他LINQ程序,此技术存在问题。它将 IQueryable转换为 IEnumerable,更改查询并可能强制进行早期评估。最好不要将它用于任何LINQ程序(如Entity Framework)。

4. 使用OzCode的LINQ功能

如果你需要有效工具调试LINQ,可以使用OzCode Visual Studio扩展。

免责声明:我目前是OzCode员工。然而,这是我个人博客,这篇文章只是我的专业推荐。

OzCode将可视化你的LINQ查询,以准确显示每个项目的行为方式。首先,它将显示每次操作后的项目数:

然后,你可以点击任何编号按钮以查看项目以及它们在操作中的进度。

我们可以看到“Jo Parker”在源中排名第4,在第一次 Where操作之后排名第3。它没有通过第二次的 Where操作。它甚至没有在最后两次操作 OrderByTake中处理。

如果这还不够,你可以按右上角的“lambda”按钮查看完整的LINQ分析。以下是它的样子:

因此,在调试LINQ方面,你几乎可以充满希望和梦想。

总结

调试LINQ不是很直观,但可以通过一些技术很好的完成。

我没有提到LINQ查询语法,因为他没有被使用太多。只有技术#2 (lambda断点)和技术#4 (OzCode)爱使用了查询语法。

我希望你能使用本文的一些技巧,请继续关注以后的帖子。


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存