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, 35
Peter 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 Mond,Peter Claus 和 Mike 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。这将仅显示与日志名称前缀匹配的行。
查看输出窗口,可以看到以下几点:
源中包括“Jose Mond”,但
logWhere
没有,这是因为我们之前看到的区分大小写的错误。由于提前使用
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
操作。它甚至没有在最后两次操作 OrderBy
和 Take
中处理。
如果这还不够,你可以按右上角的“lambda”按钮查看完整的LINQ分析。以下是它的样子:
因此,在调试LINQ方面,你几乎可以充满希望和梦想。
总结
调试LINQ不是很直观,但可以通过一些技术很好的完成。
我没有提到LINQ查询语法,因为他没有被使用太多。只有技术#2 (lambda断点)和技术#4 (OzCode)爱使用了查询语法。
我希望你能使用本文的一些技巧,请继续关注以后的帖子。