原文:How to Debug LINQ queries in C#
作者:Michael Shpilt
译文:如何在C#中调试LINQ查询
译者:Lamond Lu
在C#中我最喜欢的特性就是LINQ。使用LINQ, 我们可以获得一种易于编写和理解的简洁语法,而不是单调的foreach
循环,它可以让你的代码更加美观。
但是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);
}
这里我们使用的数据集如下:
Name | Age | Gender | Salary |
---|---|---|---|
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 |
当运行以上查询之后, 我得到的结果是
Peter Claus, 61000, 40
这个结果看起来不太对...这里应该查出3个员工。这里我们计算出的平均工资应该是56400, 所以'Jose Mond'和'Mick Mockson'应该也是满足条件的结果。
所以呢,这里在我的LINQ查询中有BUG, 那么我们该怎么做? 当然我可以一直盯着代码来找出问题,在某些场景下这种方式可能是行的通的。或者呢我们可以来尝试调试它。
下面让我们看一下,我们有哪些可选的调试方法。
1. 使用Quickwatch
这里比较容易的方法是使用QuickWatch窗口来查看查询的不同部分的结果。你可以从第一个操作开始,一步一步的追加过滤条件。
例:
这里我们可以看到,在经过第一个查询之后,就出错了。 'Jose Mond'应该是一个男性,但是在结果集中缺失了。那么我们的BUG应该就是出在这里了,我们可以只盯着这一小段代码来查找问题。没错,这里的BUG原因是数据集中将男性拼写为了'male', 而不是我们查询的'Male'。
因此,现在我可以通过忽略大小写来修复这个问题。
var res = employees
.Where(e => e.Gender.Equals("Male", StringComparison.OrdinalIgnoreCase))
.Take(3)
.Where(e => e.Salary > avgSalary)
.OrderBy(e => e.Age);
现在我们将得到如下结果集:
Jose Mond, 62000, 35
Peter Claus, 61000, 40
在结果集中'Jose'已经包含在内了,所以这里第一个Bug已经被修复了。但是问题是'Mike Mockson'依然没有出现在结果集里面。我们将使用后面的调试方式来解决它。
Quickwatch看似很美好,其实是有一个很大的缺点。如果你要从一个很大的数据集中找到一个指定的数据项,你可以需要花非常多的时间。
而且需要注意有些查询可能会改变应用的状态。例如,你可能在lambda表达式中,通过调用某个方法来改变一些变量的值,例如var res = source.Select(x => x.Age++)
。在Quickwatch中运行这段代码,你的应用状态会被修改,调试上下文会不一致。不过在Quickwatch你可以使用添加nse
这个"无副作用"标记,来避免调试上下文的变更。你可以在你的LINQ表达式后面追加, nse
的后缀来启用“无副作用”标记。
例:
2. 在lambda表达式部分放置断点
另外一种非常好用的调试方式是在lambda表达式内部放置断点。这可以让你查看每个独立数据项的值。针对比较大的数据集,你可以使用条件断点。
在我们的用例中,我们发现'Mike Mockson'不在第一个Where
操作结果集中。这时候我们就可以在.Where(e => e.Gender == "Male")
代码部分添加一个条件断点,断点条件是e.Name=="Mike Mockson"
在我们的用例中,这个断点永远不会被触发。而且在我们将查询条件改为
.Where(e => e.Gender.Equals("Male", StringComparison.OrdinalIgnoreCase))
之后也不会触发。你知道这是为什么?
现在不要在盯着代码了,这里我们使用断点的Actions功能,这个功能允许你在断点触发时,在Output窗口中输出日志。
再次调试之后,我们会在Output窗口中得到如下结果:
只有3个人名被打印出来了。这是因为在我们的查询中使用了.Take(3)
, 它会让数据集只返回前3个匹配的数据项。
这里我们本来的意愿是想列出超过平均工资的前三位男性,并且按照年龄排序。所以这里我们应该把Take
放到工资过滤代码的后面。
var res = employees
.Where(e => e.Gender.Equals("Male", StringComparison.OrdinalIgnoreCase))
.Where(e => e.Salary > avgSalary)
.Take(3)
.OrderBy(e => e.Age);
再次运行之后,结果集正确显示了Jose Mond,Peter Claus和Mike Mockson。
注: LINQ to SQL中,这个方式不起作用。
3. 为LINQ添加日志扩展方法
现在让我们把代码还原到Bug还未修复的最初状态.
下面我们来使用扩展方法来帮助调试Query。
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);
输出结果如下:
说明和解释:
LogLINQ
方法需要放在你的每个查询条件后面。它会输出所有满足条件的数据项及其总数logName
是一个输出日志的前缀,使用它可以很容易了解到当前运行的是哪一步查询Func<T, string> printMethod
是一个委托,它可以帮助打印任何你指定的变量值,在上述例子中,我们打印了员工的名字- 为了优化代码,这个代码应该是只在调试模式使用。所以我们添加了
#if DEBUG
。
下面我们来分析一下输出窗口的结果,你会发现这几个问题:
source
中包含"Jose Mond", 但是logWhere
中不包含,这就是我们前面发现的大小写问题- "Mike Mockson"没有出现在任何结果中,原因是过早的使用
Take
, 过滤了许多正确的结果。
4. 使用OzCode的LINQ功能
如果你需要一个强力的工具来调试LINQ, 那么你可以使用OzCode
这个Visual Studio插件。
OzCode可以提供一个可视化的LINQ查询界面来展示每一个数据项的行为。首先,它可以展示每次操作后,满足条件的所有数据项的数量。
然后呢,当你点击任何一个数字按钮的时候,你可以查看所有满足条件的数据项。
我们可以看到"Jo Parker"是源数据的第四个,经过第一个Where
查询时候,变成了数据源中的第三项。这里可以看到在最后2步操作OrderBy
和Take
返回的结果集中没有这一项了,因为他已经被过滤掉了。
就调试LINQ而言,OzCode基本上已经可以满足你的所有需求了。
总结
LINQ的调试不是非常直观,但是通过一些内置和第三方组件还是可以很好调试结果。
这里我没有提到LINQ查询语法,因为它使用得并不多。只有方式#2 (lambda表达式部分放置断点)和技术#4 (OzCode)可以使用查询语法。
LINQ既适用于内存集合,也适用于数据源。直接数据源可以是SQL数据库、XML模式和web服务。但是并非所有上述技术都适用于数据源。特别是,方式#2 (lambda表达式部分放置断点)根本不起作用。方式#3(日志中间件)可以用于调试,但最好避免使用它,因为它将集合从IQueryable更改为IEnumerable。不要让LogLINQ方法用于生产数据源。方式#4 (OzCode)对于大多数LINQ提供程序都可以很好地工作,但是如果LINQ提供程序以非标准的方式工作,那么可能会有一些细微的变化。