如何在C#中除錯LINQ查詢

LamondLu發表於2019-06-25

原文:How to Debug LINQ queries in C#
作者:Michael Shpilt
譯文:如何在C#中除錯LINQ查詢
譯者:Lamond Lu

如何在C#中除錯LINQ查詢

在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視窗來檢視查詢的不同部分的結果。你可以從第一個操作開始,一步一步的追加過濾條件。

例:

如何在C#中除錯LINQ查詢

這裡我們可以看到,在經過第一個查詢之後,就出錯了。 '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的字尾來啟用“無副作用”標記。

例:

如何在C#中除錯LINQ查詢

2. 在lambda表示式部分放置斷點

另外一種非常好用的除錯方式是在lambda表示式內部放置斷點。這可以讓你檢視每個獨立資料項的值。針對比較大的資料集,你可以使用條件斷點。

在我們的用例中,我們發現'Mike Mockson'不在第一個Where操作結果集中。這時候我們就可以在.Where(e => e.Gender == "Male")程式碼部分新增一個條件斷點,斷點條件是e.Name=="Mike Mockson"

如何在C#中除錯LINQ查詢

在我們的用例中,這個斷點永遠不會被觸發。而且在我們將查詢條件改為

.Where(e => e.Gender.Equals("Male", StringComparison.OrdinalIgnoreCase))

之後也不會觸發。你知道這是為什麼?

現在不要在盯著程式碼了,這裡我們使用斷點的Actions功能,這個功能允許你在斷點觸發時,在Output視窗中輸出日誌。

如何在C#中除錯LINQ查詢

再次除錯之後,我們會在Output視窗中得到如下結果:

如何在C#中除錯LINQ查詢

只有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);
 

輸出結果如下:

如何在C#中除錯LINQ查詢

說明和解釋:

  • LogLINQ方法需要放在你的每個查詢條件後面。它會輸出所有滿足條件的資料項及其總數
  • logName是一個輸出日誌的字首,使用它可以很容易瞭解到當前執行的是哪一步查詢
  • Func<T, string> printMethod是一個委託,它可以幫助列印任何你指定的變數值,在上述例子中,我們列印了員工的名字
  • 為了優化程式碼,這個程式碼應該是隻在除錯模式使用。所以我們新增了#if DEBUG

下面我們來分析一下輸出視窗的結果,你會發現這幾個問題:

  • source中包含"Jose Mond", 但是logWhere中不包含,這就是我們前面發現的大小寫問題
  • "Mike Mockson"沒有出現在任何結果中,原因是過早的使用Take, 過濾了許多正確的結果。

4. 使用OzCode的LINQ功能

如果你需要一個強力的工具來除錯LINQ, 那麼你可以使用OzCode這個Visual Studio外掛。

OzCode可以提供一個視覺化的LINQ查詢介面來展示每一個資料項的行為。首先,它可以展示每次操作後,滿足條件的所有資料項的數量。

如何在C#中除錯LINQ查詢

然後呢,當你點選任何一個數字按鈕的時候,你可以檢視所有滿足條件的資料項。

如何在C#中除錯LINQ查詢

我們可以看到"Jo Parker"是源資料的第四個,經過第一個Where查詢時候,變成了資料來源中的第三項。這裡可以看到在最後2步操作OrderByTake返回的結果集中沒有這一項了,因為他已經被過濾掉了。

如何在C#中除錯LINQ查詢

就除錯LINQ而言,OzCode基本上已經可以滿足你的所有需求了。

總結

LINQ的除錯不是非常直觀,但是通過一些內建和第三方元件還是可以很好除錯結果。

這裡我沒有提到LINQ查詢語法,因為它使用得並不多。只有方式#2 (lambda表示式部分放置斷點)和技術#4 (OzCode)可以使用查詢語法。

LINQ既適用於記憶體集合,也適用於資料來源。直接資料來源可以是SQL資料庫、XML模式和web服務。但是並非所有上述技術都適用於資料來源。特別是,方式#2 (lambda表示式部分放置斷點)根本不起作用。方式#3(日誌中介軟體)可以用於除錯,但最好避免使用它,因為它將集合從IQueryable更改為IEnumerable。不要讓LogLINQ方法用於生產資料來源。方式#4 (OzCode)對於大多數LINQ提供程式都可以很好地工作,但是如果LINQ提供程式以非標準的方式工作,那麼可能會有一些細微的變化。

相關文章