如何在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 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提供程序以非標准的方式工作,那么可能會有一些細微的變化。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM