当前位置:首页 > C#编程 > C#/.net框架 > 正文内容

如何在C#中调试LINQ查询

Jorge2年前 (2022-05-08)C#/.net框架901

在C#中我最喜欢的特性就是LINQ。使用LINQ, 我们可以获得一种易于编写和理解的简洁语法,而不是单调的foreach循环,它可以让你的代码更加美观。

但是LINQ也有不好的地方,就是调试起来非常难。我们无法知道查询中到底发生了什么。我们可以看到输入值和输出值,但是仅此而已。当代码出现问题的时候,我们只能盯着代码看吗?答案是否定的,这里有几种可以使用的LINQ的调试方法。

LINQ调试

尽管很困难,但是这里还是有几种可选的方式来调试LINQ的。

这里首先,我们先创建一个测试场景。假设我们现在想要获取一个列表,这个列表中包含了3个超过平均工资的男性员工的信息,并且按照年龄排序。这是一个非常普通的查询,下面就是我针对这个场景编写的查询方法。

C#
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);}

这里我们使用的数据集如下:

NameAgeGenderSalary
Peter Claus40"Male"61000
Jose Mond35"male"62000
Helen Gant38"Female"38000
Jo Parker42"Male"52000
Alex Mueller22"Male"39000
Abbi Black53"female"56000
Mike Mockson51"Male"82000

当运行以上查询之后, 我得到的结果是

C#
Peter Claus, 61000, 40

这个结果看起来不太对...这里应该查出3个员工。这里我们计算出的平均工资应该是56400, 所以'Jose Mond'和'Mick Mockson'应该也是满足条件的结果。

所以呢,这里在我的LINQ查询中有BUG, 那么我们该怎么做? 当然我可以一直盯着代码来找出问题,在某些场景下这种方式可能是行的通的。或者呢我们可以来尝试调试它。

下面让我们看一下,我们有哪些可选的调试方法。

1. 使用Quickwatch#

这里比较容易的方法是使用QuickWatch窗口来查看查询的不同部分的结果。你可以从第一个操作开始,一步一步的追加过滤条件。

例:

这里我们可以看到,在经过第一个查询之后,就出错了。 'Jose Mond'应该是一个男性,但是在结果集中缺失了。那么我们的BUG应该就是出在这里了,我们可以只盯着这一小段代码来查找问题。没错,这里的BUG原因是数据集中将男性拼写为了'male', 而不是我们查询的'Male'。

因此,现在我可以通过忽略大小写来修复这个问题。

C#
var res = employees        .Where(e => e.Gender.Equals("Male", StringComparison.OrdinalIgnoreCase))
        .Take(3)
        .Where(e => e.Salary > avgSalary)
        .OrderBy(e => e.Age);

现在我们将得到如下结果集:

C#
Jose Mond, 62000, 35Peter 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。

C#
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


扫描二维码推送至手机访问。

版权声明:本文由7点博客发布,如需转载请注明出处。

本文链接:http://6dot.cn/?id=62

标签: .NET.NET框架
分享给朋友:

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

C# 控件闪烁问题的解决

C# 控件闪烁问题的解决

说一下解决C#下控件闪烁的几个问题,如下:  listview和datagridview显示数据闪烁 自定义控件的显示闪烁listbox滚动条拖动闪烁面板中控件过多的闪烁propertyGrid点击和修改项目缓慢的问题richtextbox控件的刷新显示问题此类问题对于界面复杂规...

C#字符串与享元(Flyweight)模式

C#字符串与享元(Flyweight)模式

注:关注这个话题是因为看到C#的关键字 lock时,其传入引用对象。因为string也是引用对象,所以能否做为lock的参数?对于这个问题,要搞明白C#的字符串的一个特点,它使用类似于享元模式的机制。因此在lock中锁字符串是相当不安全的。下面贴子是对C#字符串与享元模式的深入讨论。写这个文章,主要...

C# 不要阻塞异步代码,即异步代码死锁的最佳解决方案

C# 不要阻塞异步代码,即异步代码死锁的最佳解决方案

这是一个在论坛和 Stack Overflow 上反复提出的问题。我认为这是异步新手在学习了基础知识后最常问的问题。用户界面示例我们编写了下面的例子。单击按钮将启动 REST 调用并在文本框中显示结果(此示例适用于 Windows 窗体,但相同的原则适用于任何UI 应用程序)。C#using&nbs...

细说进程、应用程序域与上下文之间的关系(四)——进程应用程序域与线程的关系

细说进程、应用程序域与上下文之间的关系(四)——进程应用程序域与线程的关系

目录一、进程的概念与作用二、应用程序域三、深入了解.NET上下文四、进程应用程序域与线程的关系 四、进程、应用程序域、线程的相互关系4.1 跨AppDomain运行代码在应用程序域之间的数据是相对独立的,当需要在其他AppDomain当中执行当前 AppDomain中的程序集代码时,可以使...

C#中CancellationToken和CancellationTokenSource用法

C#中CancellationToken和CancellationTokenSource用法

 继续谈一下异步中的任务取消机制CancellationToken和CancellationTokenSource。  之前做开发时,一直没注意这个东西,做了.net core之后,发现CancellationToken用的越来越平凡了。  这也难怪,原来.net framework使用异...

C# 外观模式(Facade)

C# 外观模式(Facade)

1. 外观模式简介  外观模式主要解决的问题:当有多个类要处理时,需要一个个类去调用,没有复用性和扩展性。外观模式将处理子类的过程封装成操作,简化客户端的调用过程。1.1 定义  外观模式(Facade)通过提供一个统一接口,来访问子系统的多个接口。  使用外观模式时,创建一个统一的类,用来包装子系...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。