在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提供程序以非标准的方式工作,那么可能会有一些细微的变化。
转载自:https://www.cnblogs.com/lwqlun/p/11083647.html
勇哥注:
为了方便大家,勇哥把本文的测试程序敲了出来放上来。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Diagnostics; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace WindowsFormsApp1 { public partial class Form1 : Form { public List<Employee> employees = new List<Employee>() { new Employee(){ Age=40, Name="Peter Claus", Gender="Male", Salary=61000}, new Employee(){ Age=35, Name="Jose Mond", Gender="Male", Salary=62000}, new Employee(){ Age=38, Name="Helen Gant", Gender="Female", Salary=38000}, new Employee(){ Age=42, Name="Jo Parker", Gender="Male", Salary=52000}, new Employee(){ Age=22, Name="Alex Mueller", Gender="Male", Salary=39000}, new Employee(){ Age=53, Name="Abbi Black", Gender="female", Salary=56000}, new Employee(){ Age=51, Name="Mike Mockson", Gender="Male", Salary=82000} }; List<string> list1 = new List<string>() { "abc", "efg", "hij", "mln" }; public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { //foreach (var m in NextStr()) //{ // richTextBox1.AppendText(m); //} //var s = NextStr(); //richTextBox1.AppendText(s.ToArray()[0]); // NextStr().GetEnumerator().MoveNext(); MyQuery(employees); } public struct Employee { public string Name; public int Age; public string Gender; public int Salary; } 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); } private IEnumerable<string> NextStr() { foreach (var m in list1) yield return m; } } public static class extendMethod { 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 } } }

