如何在C#中调试LINQ查询
原文: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提供程序以非标准的方式工作,那么可能会有一些细微的变化。
如何在C#中调试LINQ查询的更多相关文章
- LINQ查询表达式(2) - 在 C# 中编写 LINQ 查询
在 C# 中编写 LINQ 查询 C# 中编写 LINQ 查询的三种方式: 使用查询语法. 使用方法语法. 组合使用查询语法和方法语法. // 查询语法 IEnumerable<int> ...
- Rafy 中的 Linq 查询支持(根据聚合子条件查询聚合父)
为了提高开发者的易用性,Rafy 领域实体框架在很早开始就已经支持使用 Linq 语法来查询实体了.但是只支持了一些简单的.常用的条件查询,支持的力度很有限.特别是遇到对聚合对象的查询时,就不能再使用 ...
- 如何在VC++ 中调试MEX文件
MEX文件对应的是将C/C++文件语言的编写之后 得到的相关文件加载到Matlab中运行的一种方式, 现对于Matlab 中的某些程序运行效率而言, C/C++ 代码某些算法的领域上面执行效率很高,若 ...
- Unity3D C#中使用LINQ查询(与 SQL的区别)
学过SQL的一看就懂 LINQ代码很直观 但是,LINQ却又跟SQL完全不同 首先来看一下调用LINQ的代码 int[] badgers = {36,5,91,3,41,69,8}; var skun ...
- .Net,Dll扫盲篇,如何在VS中调试已经编译好的dll?
什么是Dll? DLL 是一个包含可由多个程序同时使用的代码和数据的库. 例如,在 Windows 操作系统中,Comdlg32 DLL 执行与对话框有关的常见函数.因此,每个程序都可以使用该Dll中 ...
- linux系统下如何在vscode中调试C++代码
本篇博客以一个简单的hello world程序,介绍在vscode中调试C++代码的配置过程. 1. 安装编译器 vscode是一个轻量的代码编辑器,并不具备代码编译功能,代码编译需要交给编译器完成. ...
- 使用“Cocos引擎”创建的cpp工程如何在VS中调试Cocos2d-x源码
前段时间Cocos2d-x更新了一个Cocos引擎,这是一个集合源码,IDE,Studio这一家老小的整合包,我们可以使用这个Cocos引擎来创建我们的项目. 在Cocos2d-x被整合到Cocos引 ...
- 如何在IDEA中调试 Jar文件
原创文章,转载请注明出处:http://www.cnblogs.com/acm-bingzi/p/6668333.html 问题: 一般情况下,可以打成Jar包的项目,它的源码运行Applicat ...
- 如何在idea中调试spring bean
步骤 在 Run/Debug Confihuration 中,增加 Application -> local,除去其余配置外,在 Program arguments 一栏添加以下字段:javac ...
随机推荐
- C#在VS2005开发环境中利用异步模式来对一个方法的执行时间进行超时控制
using System.Threading; using System; namespace ConsoleApplication4 { public class Program { static ...
- Java算法面试题 一个顺子带一对
打牌里面经常出现的5张牌,一个顺子带一对,给你五张牌,比如:1,2,2,2,3 或者 5,6,7,4,4 或者 2,4,3,5,5 或者 7,5,9,6,9 ,这种情况就符合一个顺子带一对,则返回 t ...
- hibernate课程 初探单表映射1-7 hibernate配置文件新建
hibernate 配置文件新建 1 右键src==>new==>other==>hibernate configuration File==>next==>next= ...
- HDU1043 八数码(BFS + 打表)
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1043 , 康托展开 + BFS + 打表. 经典八数码问题,传说此题不做人生不完整,关于八数码的八境界 ...
- SAP Cloud for Customer Extensibility的设计与实现
今天的文章来自Jerry的同事,SAP成都研究院C4C开发团队的开发人员徐欢(Xu Boris).徐欢就坐我左手边的位置,因此我工作中但凡遇到C4C的技术问题,一扭头就可以请教他了,非常方便.下图是他 ...
- POJ 3280 Cheapest Palindrome(区间dp)
dp[i][j]表示处理完i到j的花费,如果s[i] == s[j] 则不需要处理,否则处理s[i]或s[j], 对一个字符ch,加上ch或删掉ch对区间转移来说效果是一样的,两者取min. #inc ...
- 【BZOJ4571】[SCOI2016] 美味(主席树)
点此看题面 大致题意: 给你一个序列\(a\),然后每次询问\(max_{i=l}^r(a_i+x)\ xor\ b\). 大致思路 首先,我们要知道一个简单的性质:位运算时位与位之间是互不影响的. ...
- Java 发送 Https 请求工具类 (兼容http)
依赖 jsoup-1.11.3.jar <dependency> <groupId>org.jsoup</groupId> <artifactId>js ...
- C# FileStream对象
FileStream对象表示在磁盘或网络路径上指向文件的流.当类提供向文件读写字节的方法时,经常使用StreamReader或StreamWriter执行这些功能.这是因为FileStream类操作字 ...
- 软件杯python-flask遇到的坑有感!
大三下,对于我考研的人来说,时间不要太紧张,参加软件杯也是系主任要求,题目是公共地点人流量的检测,个人还是个菜鸟,但是把遇到的一些大家可能不小心会出现的问题贴出来,困扰我很久,还没睡好觉!!! Que ...