原文: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 ClausMike 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步操作OrderByTake返回的结果集中没有这一项了,因为他已经被过滤掉了。

就调试LINQ而言,OzCode基本上已经可以满足你的所有需求了。

总结

LINQ的调试不是非常直观,但是通过一些内置和第三方组件还是可以很好调试结果。

这里我没有提到LINQ查询语法,因为它使用得并不多。只有方式#2 (lambda表达式部分放置断点)和技术#4 (OzCode)可以使用查询语法。

LINQ既适用于内存集合,也适用于数据源。直接数据源可以是SQL数据库、XML模式和web服务。但是并非所有上述技术都适用于数据源。特别是,方式#2 (lambda表达式部分放置断点)根本不起作用。方式#3(日志中间件)可以用于调试,但最好避免使用它,因为它将集合从IQueryable更改为IEnumerable。不要让LogLINQ方法用于生产数据源。方式#4 (OzCode)对于大多数LINQ提供程序都可以很好地工作,但是如果LINQ提供程序以非标准的方式工作,那么可能会有一些细微的变化。

如何在C#中调试LINQ查询的更多相关文章

  1. LINQ查询表达式(2) - 在 C# 中编写 LINQ 查询

    在 C# 中编写 LINQ 查询 C# 中编写 LINQ 查询的三种方式: 使用查询语法. 使用方法语法. 组合使用查询语法和方法语法. // 查询语法 IEnumerable<int> ...

  2. Rafy 中的 Linq 查询支持(根据聚合子条件查询聚合父)

    为了提高开发者的易用性,Rafy 领域实体框架在很早开始就已经支持使用 Linq 语法来查询实体了.但是只支持了一些简单的.常用的条件查询,支持的力度很有限.特别是遇到对聚合对象的查询时,就不能再使用 ...

  3. 如何在VC++ 中调试MEX文件

    MEX文件对应的是将C/C++文件语言的编写之后 得到的相关文件加载到Matlab中运行的一种方式, 现对于Matlab 中的某些程序运行效率而言, C/C++ 代码某些算法的领域上面执行效率很高,若 ...

  4. Unity3D C#中使用LINQ查询(与 SQL的区别)

    学过SQL的一看就懂 LINQ代码很直观 但是,LINQ却又跟SQL完全不同 首先来看一下调用LINQ的代码 int[] badgers = {36,5,91,3,41,69,8}; var skun ...

  5. .Net,Dll扫盲篇,如何在VS中调试已经编译好的dll?

    什么是Dll? DLL 是一个包含可由多个程序同时使用的代码和数据的库. 例如,在 Windows 操作系统中,Comdlg32 DLL 执行与对话框有关的常见函数.因此,每个程序都可以使用该Dll中 ...

  6. linux系统下如何在vscode中调试C++代码

    本篇博客以一个简单的hello world程序,介绍在vscode中调试C++代码的配置过程. 1. 安装编译器 vscode是一个轻量的代码编辑器,并不具备代码编译功能,代码编译需要交给编译器完成. ...

  7. 使用“Cocos引擎”创建的cpp工程如何在VS中调试Cocos2d-x源码

    前段时间Cocos2d-x更新了一个Cocos引擎,这是一个集合源码,IDE,Studio这一家老小的整合包,我们可以使用这个Cocos引擎来创建我们的项目. 在Cocos2d-x被整合到Cocos引 ...

  8. 如何在IDEA中调试 Jar文件

    原创文章,转载请注明出处:http://www.cnblogs.com/acm-bingzi/p/6668333.html   问题: 一般情况下,可以打成Jar包的项目,它的源码运行Applicat ...

  9. 如何在idea中调试spring bean

    步骤 在 Run/Debug Confihuration 中,增加 Application -> local,除去其余配置外,在 Program arguments 一栏添加以下字段:javac ...

随机推荐

  1. 表格<table>

    <table> <tr> <th>表头1</th> <th>表头2</th> <th>表头3</th> ...

  2. visual studio2010中C#生成的,ArcGIS二次开发的basetool的dll,注册为COM组件tlb文件,并在arcmap中加载使用

    写了个标题好长啊~~~~ 这两天又认识了一个新玩意,记录一下下,啦啦啦~~~~~ 话说,认识arcgis快十年了,从桌面版到engine的二次开发,其实不过才认识到它的冰山一角, 它总是能带来很多还未 ...

  3. Android 5.0以上获取系统运行进程信息

    在Android 5.0以上系统,调用getRunningAppProcesses 方法返回的列表为空,这是因为谷歌考虑到安全原因,已经把这个方法移除掉了, 那以后要获取系统运行的后台进程这个方法用不 ...

  4. iOS - 毛玻璃动画效果

    声明全局变量 #define kMainBoundsHeight ([UIScreen mainScreen].bounds).size.height //屏幕的高度 #define kMainBou ...

  5. 初识ListView - 定制ListView - 提升ListView运行效率

    ListView绝对可以称得上是 Android 中最常用的控件之一,几乎所有的应用程序都会用到它.由于手机屏幕空间都比较有限,能够一次性在屏幕上显示的内容并不多,当我们的程序中有大量的数据需要展示的 ...

  6. 部署webservice到远程服务器

    在本地编写好webservice后并在本机验证正确后,在本地发布后,直接将发布时设置的文件夹复制到远程服务器上,在远程服务器的IIS上默认网站->新建虚拟目录->设置别名->物理路径 ...

  7. 模块详解及import本质

    一.模块的定义 用来从逻辑上组织python代码(变量,函数,类,逻辑:实现一个功能) 本质就是.py结尾的Python文件(文件名test.py,对应的模块名:test) 包:用来从逻辑上组织模块的 ...

  8. WebUploader实现采集图片的功能

    项目最开始用百度团队的文件上传组件做了个物料照片采集的功能,后来做员工头像采集时竟然不知道怎么使用了. 参照官方Demo: http://fex.baidu.com/webuploader/getti ...

  9. spring boot 下 spring security 自定义登录配置与form-login属性详解

    package zhet.sprintBoot; import org.springframework.beans.factory.annotation.Autowired;import org.sp ...

  10. help.hybris.com和help.sap.com网站的搜索实现

    help.hybris.com 我使用help.hybris.com时,发现每次在搜索栏输入文字时,没有发出任何HTTP请求,那么这个自动完成的下拉框里的记录从哪里来的?我看了下实现,发现所有自动完成 ...