原文: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 Claus和Mike 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步操作OrderBy
和Take
返回的結果集中沒有這一項了,因為他已經被過濾掉了。
就除錯LINQ而言,OzCode基本上已經可以滿足你的所有需求了。
總結
LINQ的除錯不是非常直觀,但是通過一些內建和第三方元件還是可以很好除錯結果。
這裡我沒有提到LINQ查詢語法,因為它使用得並不多。只有方式#2 (lambda表示式部分放置斷點)和技術#4 (OzCode)可以使用查詢語法。
LINQ既適用於記憶體集合,也適用於資料來源。直接資料來源可以是SQL資料庫、XML模式和web服務。但是並非所有上述技術都適用於資料來源。特別是,方式#2 (lambda表示式部分放置斷點)根本不起作用。方式#3(日誌中介軟體)可以用於除錯,但最好避免使用它,因為它將集合從IQueryable更改為IEnumerable。不要讓LogLINQ方法用於生產資料來源。方式#4 (OzCode)對於大多數LINQ提供程式都可以很好地工作,但是如果LINQ提供程式以非標準的方式工作,那麼可能會有一些細微的變化。