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