上一篇終於把中斷點下下去了~
F9
下 Breakpoint
F5
開始 Debug
F10
往下一行
F11
進入方法

然後 Visual Studio 真的停住了。
結果我又出現新的問題:
好。
它停了。
然後我要看什麼?
程式停在 Breakpoint 之後,其實才是 Debug 真正開始的地方。
這時除了黃色那一行程式碼之外,我還可以去看:
目前變數是多少?
物件裡面有哪些資料?
資料在哪一步開始變錯?
程式到底是從哪個方法一路跑到這裡的?
Visual Studio 也有幾個專門拿來看這些資訊的「除錯視窗」:
Locals
區域變數
Watch
監看式
Call Stack
呼叫堆疊
這幾個視窗通常要先讓程式跑到 Breakpoint,進入「偵錯暫停狀態」之後,再從:
偵錯
↓
視窗
↓
Locals / Watch / Call Stack
開啟。

第一次看到一堆:
Locals
Watch
Call Stack
Autos
Immediate Window
真的很容易資訊過量。
但新手 Debug 不用一次全部會。
我目前最常用的其實就是:
直接看變數
Locals
看目前有哪些變數和值
Watch
盯著特定資料看它怎麼變
Call Stack
看程式是怎麼一路跑到這裡
這篇就用一個「畫面資料沒有正常顯示」的情境,實際看看這幾個工具怎麼用。
假設現在畫面上的:
部門名稱
沒有顯示。
Controller:
public IActionResult Detail(int id)
{
var model = _userService.GetDetail(id);
return View(model);
}
Service:
public UserDetailViewModel GetDetail(int id)
{
var user = _userRepository.GetById(id);
var model = new UserDetailViewModel
{
Name = user.Name,
Email = user.Email,
DepartmentName = user.DepartmentName
};
return model;
}
View:
@model UserDetailViewModel
<h1>@Model.Name</h1>
<p>@Model.Email</p>
<p>@Model.DepartmentName</p>
現在問題是:
Name 有顯示
Email 有顯示
DepartmentName 卻是空的
如果只看 View,我只知道:
@Model.DepartmentName
沒有資料。
但真正的問題可能出現在很多地方:
Database
↓
Repository
↓
Entity / DTO
↓
Service
↓
ViewModel
↓
Controller
↓
View
所以這次不要猜。
直接 Debug。
假設我在 Controller:
public IActionResult Detail(int id)
{
var model = _userService.GetDetail(id);
return View(model);
}
這一行下中斷點:
var model = _userService.GetDetail(id);
接著按:
F5
然後操作到:
/User/Detail/10
Visual Studio 停下來。
這時候最簡單的方法,就是直接把滑鼠移到:
id
上面。
可能會看到:
id = 10
這馬上就能確認:
網址傳進來的 id
是不是我預期的值?
假設我原本以為正在查:
id = 10
結果 Debug 發現:
id = 0
那問題可能根本還沒進到 Service 或資料庫。
而是前面的:
Route
網址參數
參數名稱
表單資料
就已經出問題了。
不只是:
int
string
bool
這種簡單變數可以看。
物件也可以。
例如先按:
F10
把這行執行完:
var model = _userService.GetDetail(id);
現在程式停在:
return View(model);
這時把滑鼠移到:
model
上面。
可能會看到:
model
├─ Name = "Amy"
├─ Email = "amy@example.com"
└─ DepartmentName = null
這個資訊就很重要了。
因為現在已經知道:
Controller 準備傳給 View 的 model
DepartmentName 就已經是 null
所以問題不太可能只是:
View 沒有把 DepartmentName 顯示出來
因為 View 根本沒有拿到值。
這時候就要往前找:
Service
Repository
Database
這也是我現在 Debug 很重要的一個觀念:
先找到資料從哪一步開始不對
而不是看到哪裡就改哪裡。
當程式停在 Breakpoint 時,可以開:
Locals
區域變數
從:
偵錯
↓
視窗
↓
區域變數
開啟。
Locals 可以先理解成:
把目前這個執行位置附近可以使用的區域變數和值列給我看。
假設現在停在:
public UserDetailViewModel GetDetail(int id)
{
var user = _userRepository.GetById(id);
var model = new UserDetailViewModel
{
Name = user.Name,
Email = user.Email,
DepartmentName = user.DepartmentName
};
return model;
}
Locals 裡可能會看到:
id
user
model
this
物件還可以繼續展開。
例如:
user
├─ Id = 10
├─ Name = "Amy"
├─ Email = "amy@example.com"
└─ DepartmentName = "資訊部"
接著看:
model
├─ Name = "Amy"
├─ Email = "amy@example.com"
└─ DepartmentName = null
蛤?
這時候問題範圍就已經縮小很多了。
因為:
user.DepartmentName
明明有:
資訊部
但:
model.DepartmentName
卻是:
null
代表問題很可能發生在:
Entity / DTO
↓
ViewModel
這段資料轉換。
例如實際程式可能漏寫:
DepartmentName = user.DepartmentName
我會把 Locals 理解成:
現在停在這個方法裡,
幫我把目前附近可以看的變數
全部攤出來。
所以它很適合拿來看:
方法收到的參數
查詢後拿到的物件
ViewModel 轉換前後的資料
bool 狀態
List 有幾筆資料
例如:
var users = _userRepository.GetUsers();
執行完後,可以在 Locals 展開:
users
看:
Count = 0
還是:
Count = 20
這比只看程式碼猜:
應該有查到吧?
可靠很多。
其實兩個都是在看:
目前執行時的資料
差別主要在使用方式。
滑鼠移到變數:
快速看某一個值
Locals:
一次看目前很多變數
如果我只是想知道:
id 現在是多少?
直接把滑鼠移過去就很方便。
但如果方法裡同時有:
id
user
department
model
result
很多東西要一起比較。
Locals 就會比較好用。
下一個很實用的是:
Watch
監看式
可以從:
偵錯
↓
視窗
↓
監看式
↓
監看式 1
開啟。
Watch 和 Locals 最大的不同是:
Locals
Visual Studio 幫我列出目前有哪些變數
Watch
我自己指定「我想一直看哪些東西」
例如現在我只關心:
id
user.DepartmentName
model.DepartmentName
就可以把它們加進 Watch。
接著按:
F10
F11
一步一步執行。
Watch 就會一直顯示這些值目前是多少。
例如:
var user = _userRepository.GetById(id);
var model = new UserDetailViewModel
{
Name = user.Name,
Email = user.Email,
DepartmentName = user.DepartmentName
};
ProcessUser(model);
return model;
假設:
剛建立 model 時
DepartmentName = "資訊部"
但是執行:
ProcessUser(model);
之後突然變成:
DepartmentName = null
如果 Watch 一直放著:
model.DepartmentName
就很容易發現:
是在 ProcessUser(model)
執行之後才變掉。
那接下來就可以:
F11
↓
進入 ProcessUser()
繼續追。
所以我會把 Watch 想成:
把重要變數釘在旁邊
讓我一路看著它怎麼變
Watch 也可以放一些運算式。
例如:
users.Count
或:
model == null
也可以直接看:
user.DepartmentName
所以 Debug List 時,不一定要每次都把整個物件一層一層展開。
可以直接監看:
users.Count
例如:
執行查詢前
users.Count = 0
執行查詢後
users.Count = 15
就會很清楚。
初學階段先會看:
變數
物件屬性
Count
其實就很夠用了。
我自己會這樣記:
Locals
=
現在這個方法裡「有什麼」
Watch
=
我現在「特別想盯什麼」
例如剛進一個陌生方法:
先看 Locals
因為可能連目前有哪些變數都不知道。
等到發現:
DepartmentName 很可疑
再把:
user.DepartmentName
model.DepartmentName
加進 Watch。
接著一路追。
這個我一開始覺得最抽象。
Call Stack
中文叫:
呼叫堆疊
名字聽起來很可怕。
但我後來用最簡單的方法理解:
我現在為什麼會跑到這個方法?是從哪裡一路被呼叫進來的?
例如現在 Breakpoint 停在:
UserRepository.GetById()
可能已經一路:
Controller
↓
Service
↓
Repository
追到有點忘記自己在哪裡。
這時打開:
偵錯
↓
視窗
↓
呼叫堆疊
可能會看到:
UserRepository.GetById(int id)
UserService.GetDetail(int id)
UserController.Detail(int id)
意思就是:
現在停在:
UserRepository.GetById()
↑
它是被:
UserService.GetDetail()
呼叫
↑
而 UserService.GetDetail()
又是被:
UserController.Detail()
呼叫
所以 Call Stack 可以先理解成:
這一次程式實際走進來的呼叫路線
假設專案裡有一個方法:
GetUser()
但很多地方都會使用它。
例如:
UserController
ReportService
AdminController
BackgroundJob
現在我想知道:
這一次到底是誰呼叫 GetUser()?
這時 Call Stack 就很好用。
前面有學過:
Shift + F12
可以找:
哪些地方有參考這個方法
但那代表:
哪些地方「可能」會使用它
Call Stack 則是在 Debug 執行當下告訴我:
這一次實際是從哪裡一路呼叫進來
所以可以簡單記:
Shift + F12
↓
誰有可能呼叫我?
Call Stack
↓
這一次到底是誰呼叫我?
這兩個用途不一樣。
假設現在停在:
UserRepository.GetById()
Call Stack 裡看到:
UserRepository.GetById()
UserService.GetDetail()
UserController.Detail()
就可以查看上一層:
UserService.GetDetail()
或再上一層:
UserController.Detail()
這在:
F11
F11
F11
一路鑽進很多方法之後很好用。
至少可以知道:
我到底是怎麼跑到這裡的?
現在把它們真的串起來。
問題:
畫面的 DepartmentName 沒有顯示
public IActionResult Detail(int id)
{
var model = _userService.GetDetail(id);
return View(model);
}
先確認:
id = 10
正常。
public UserDetailViewModel GetDetail(int id)
{
var user = _userRepository.GetById(id);
...
}
現在開始看:
id
user
user.DepartmentName
執行:
var user = _userRepository.GetById(id);
結果看到:
user.DepartmentName = "資訊部"
代表:
資料至少有查回來
所以這時不用急著跑去改 SQL。
假設實際程式變成:
var model = new UserDetailViewModel
{
Name = user.Name,
Email = user.Email
};
注意,這裡漏了:
DepartmentName = user.DepartmentName
再看:
user.DepartmentName
= "資訊部"
model.DepartmentName
= null
問題就抓到了。
資料是在:
Entity / DTO
↓
ViewModel
轉換這一步掉的。
不是:
View
也不是:
Database
以前看到:
畫面 DepartmentName 空白
可能會同時懷疑:
HTML?
Razor?
Controller?
Service?
SQL?
Database?
範圍超大。
但 Debug 之後可以一層一層確認:
Database 查出來有值嗎?
↓
有
Repository 回來有值嗎?
↓
有
Service 裡 user 有值嗎?
↓
有
轉成 model 後還有嗎?
↓
沒有
問題範圍縮小
所以真正要找的是:
最後一個「還正常」的位置
+
第一個「開始不正常」的位置
問題通常就在這兩個位置之間。
這個觀念比單純記住:
F10
F11
更重要。
例如:
public UserListViewModel GetList()
{
var users = _userRepository.GetUsers();
var model = new UserListViewModel
{
Users = users
};
return model;
}
結果畫面顯示:
查無資料
我可以看:
users
users.Count
model.Users.Count
Repository 執行完:
users.Count = 0
那問題可能要往:
Repository
SQL
查詢條件
Database
繼續找。
如果:
users.Count = 20
但:
model.Users.Count = 0
那問題比較可能在:
資料轉換
如果:
model.Users.Count = 20
Controller 也拿到 20 筆。
但 View 還是顯示:
查無資料
這時才往:
View
繼續找。
這樣每一步都有證據。
看到:
null
先不要急著補:
if (xxx != null)
我現在會先問:
1. 它本來就允許是 null 嗎?
2. 它應該在哪一步被賦值?
3. 前一個步驟有沒有資料?
例如:
var user = _userRepository.GetById(id);
結果:
user = null
我就會先確認:
id 是多少?
↓
Repository 查詢條件是什麼?
↓
資料庫真的有這筆資料嗎?
而不是直接在下一行寫:
if (user == null)
{
return null;
}
當然正式程式本來就可能需要 null 防護。
但 Debug 時第一件事情還是:
先搞清楚它為什麼是 null
Debug 時可能還會看到:
Autos
它會自動列出 Visual Studio 判斷目前附近可能有用的變數。
它不是不能用。
只是我目前比較常使用:
Locals
Watch
Call Stack
所以新手階段不用看到:
Autos
Locals
Watch
Call Stack
Immediate
Modules
Threads
就覺得:
這些全部都要會才能 Debug?
不用。
目前先把:
直接看變數
Locals
Watch
Call Stack
用熟,就已經非常夠用了。
如果今天畫面資料不對,我大概會這樣做:
第一步
在我認為的入口下 Breakpoint
↓
第二步
確認 Request 傳進來的參數
例如 id、model
↓
第三步
F10 / F11 往下執行
↓
第四步
用 Locals 看目前有哪些資料
↓
第五步
把重要欄位加入 Watch
↓
第六步
比較方法執行前後的值
↓
第七步
不知道自己怎麼跑到這裡
看 Call Stack
↓
第八步
找到:
最後正常的位置
+
第一個異常的位置
這時問題範圍通常就已經縮小很多。
前幾天學的東西其實不是分開的。
現在可以組成一套真正的追程式流程。
先找程式:
F12
↓
這個方法寫在哪裡?
確認會不會執行:
Breakpoint
↓
這次真的有跑進來嗎?
看目前資料:
滑鼠變數提示 / Locals
↓
現在值是多少?
持續追關鍵資料:
Watch
↓
這個值在哪一步開始改變?
看執行路線:
Call Stack
↓
這次是從哪個方法一路進來的?
這幾個工具一起使用之後,比單純盯著程式碼猜有用很多。
程式停在 Breakpoint 之後,不是只盯著黃色那一行發呆。
我現在主要會看四件事。
第一個:
直接看變數
這個參數現在是多少?
這個物件是不是 null?
第二個:
Locals
目前這個執行位置附近
有哪些區域變數和值?
第三個:
Watch
我現在最關心哪些資料?
一路執行時它們怎麼變?
第四個:
Call Stack
我現在為什麼會跑到這裡?
這一次是誰一路呼叫進來的?
真正 Debug 時,我想找的其實是:
資料最後一次正常是在哪裡?
第一次開始錯又是在哪裡?
例如:
Repository
DepartmentName = "資訊部"
↓
Service 轉 ViewModel
↓
ViewModel
DepartmentName = null
這樣就知道:
不是整個系統都壞掉
而是這兩個步驟之間出問題
對我來說,這才是開始真正會 Debug 的感覺。
不是看到問題後一直猜:
是不是這裡?
是不是那裡?
而是:
停下來
看資料
一步一步縮小範圍
接下來還有另一種很常遇到的情況。
不是:
資料怪怪的
而是程式直接:
Build Failed
或執行到一半跳出:
Exception
但 Visual Studio 裡又有:
Error List
Output
Exception
到底應該先看哪一個?
下一篇:
Day 25|Visual Studio 報錯到底先看哪裡?Error List、Output 和 Exception 怎麼分?