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

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益