5分钟学透如何在C#中调试LINQ查询 | 大白技术控

  • 2020 年 3 月 31 日
  • 筆記

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

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

调试LINQ

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

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

public IEnumerable<Employee> MyQuery(List<Employee> employees){    var avgSalary = employees.Select(e=>e.Salary).Average();      return employees        .Where(e => e.Gender == "Male")        .Take(3)        .Where(e => e.Salary > avgSalary)        .OrderBy(e => e.Age);}

数据集为:

姓名

年龄

性别

收入

Peter Claus

40

“Male”

61000

Jose Mond

35

"male"

62000

Helen Gant

38

"Female"

38000

Jo Parker

42

"Male"

52000

Alex Mueller

22

"Male"

39000

Abbi Black

53

"female"

56000

Mike Mockson

51

"Male"

82000

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

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

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

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

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

这里有一个例子:

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

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

var res = employees        .Where(e => e.Gender.ToLower() == "male") // added "ToLower()"        .Take(3)        .Where(e => e.Salary > avgSalary)        .OrderBy(e => e.Age);

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

Jose Mond, 62000, 35Peter 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运算符。将查询改为一下内容:

var res = employees        .Where(e => e.Gender.ToLower() == "male")        .Where(e => e.Salary > avgSalary)        .Take(3)        .OrderBy(e => e.Age);

正确的结果是:Jose MondPeter ClausMike Mockson

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

3. 使用日志中间件方法

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

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

public static IEnumerable<T> LogLINQ<T>(this IEnumerable<T> enumerable, string logName, Func<T, string> printMethod){#if DEBUG    int count = 0;    foreach (var item in enumerable)    {        if (printMethod != null)        {            Debug.WriteLine($"{logName}|item {count} = {printMethod(item)}");        }        count++;        yield return item;    }    Debug.WriteLine($"{logName}|count = {count}");#else       return enumerable;#endif}

以下是如何使用它:

var res = employees        .LogLINQ("source", e=>e.Name)        .Where(e => e.Gender == "Male")        .LogLINQ("logWhere", e=>e.Name)        .Take(3)        .LogLINQ("logTake", e=>e.Name)        .Where(e => e.Salary > avgSalary)        .LogLINQ("logWhere2", e=>e.Name)        .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方面,你几乎可以充满希望和梦想。