C#程序员必备的10个VS调试技巧
1. 条件断点
如何操作
-
在代码行号左侧边缘点击设置普通断点(红点)
-
右键点击红点 → 选择“条件”
-
在弹出的对话框中输入条件表达式
三种条件类型
| 类型 | 示例 | 说明 |
|---|---|---|
| 条件表达式 | i == 100 |
只有当表达式为true时中断 |
| 命中次数 | 命中次数等于 5 |
第5次执行时中断 |
| 筛选器 | MachineName = "DEV-PC" |
指定机器/进程/线程才中断 |
实用场景
// 场景1:循环中只调试特定索引
for (int i = 0; i < 1000; i++)
{
ProcessItem(i); // 只在 i == 500 时断点
}
// 场景2:调试特定输入值
public void ProcessOrder(Order order)
{
// 只在 order.Total > 10000 时中断
ValidateOrder(order);
}
// 场景3:追踪变量状态变化
private int _counter;
public void Increment()
{
_counter++; // 只在 _counter == 5 时中断
}
高级用法:使用函数返回值作为条件
// 可以直接调用方法作为条件表达式
// 例如:customer.GetTotalOrders() > 10
2. 跟踪点 (Tracepoint)
与普通断点的区别
-
普通断点:程序暂停执行
-
跟踪点:程序继续执行,只输出日志
如何设置
-
右键断点红点 → 选择“操作”
-
勾选 “将消息输出到输出窗口”
-
输入要输出的内容
特殊占位符
| 占位符 | 含义 | 示例输出 |
|---|---|---|
$ADDRESS |
当前指令地址 | 0x7FFA1234 |
$CALLER |
调用者方法名 | Main |
$FUNCTION |
当前函数名 | ProcessData |
$PID |
进程ID | 12345 |
$TID |
线程ID | 7 |
{变量名} |
变量值 | 10 |
实际示例
// 输出内容示例
"进入 ProcessItem,索引={i},当前线程={$TID}"
// 输出结果:
// 进入 ProcessItem,索引=0,当前线程=1
// 进入 ProcessItem,索引=1,当前线程=1
优势
-
无需修改代码添加
Console.WriteLine -
无需重新编译
-
可随时开启/关闭
3. 数据断点
适用场景
只能用于内存地址,主要在非托管代码或特定场景中追踪变量变化。在托管C#中,推荐使用以下替代方案:
托管代码追踪变量修改的方法
方法1:使用属性包装
private int _value;
public int Value
{
get => _value;
set
{
if (_value != value) // 在此处设置断点
{
_value = value;
}
}
}
方法2:使用 System.Diagnostics.Debugger.Break()
private int _balance;
public int Balance
{
get => _balance;
set
{
if (_balance != value) // 值即将改变
{
System.Diagnostics.Debugger.Break(); // 自动中断
_balance = value;
}
}
}
方法3:PostSharp或Fody的PropertyChanged
[AddINotifyPropertyChanged]
public class ViewModel
{
// 框架会在任何属性变化时触发事件
// 可以在事件处理中设置断点
public int Counter { get; set; }
}
4. 并行监视窗口
打开方式
调试运行时 → 调试 → 窗口 → 并行监视 → 并行监视1
视图解析
┌─────────────────┬─────────┬─────────┬─────────┐ │ 表达式 │ 线程0 │ 线程1 │ 线程2 │ ├─────────────────┼─────────┼─────────┼─────────┤ │ index │ 100 │ 200 │ 300 │ │ result │ 2.5 │ 3.2 │ 1.8 │ │ list.Count │ 1000 │ 1000 │ 1000 │ └─────────────────┴─────────┴─────────┴─────────┘
实战调试多线程问题
// 演示问题:多线程数据竞争
private static int _sharedCounter = 0;
private static readonly object _lock = new object();
static void Main()
{
Parallel.For(0, 1000, i =>
{
// 有问题的代码:缺少锁
_sharedCounter++; // 数据竞争!
// 修复后:加锁
// lock(_lock) { _sharedCounter++; }
});
Console.WriteLine(_sharedCounter); // 期望1000,实际可能小于1000
}
并行任务窗口
调试 → 窗口 → 并行任务 → 查看所有Task的状态、位置、父任务关系
常用技巧
// 在并行监视中输入
"Thread.CurrentThread.ManagedThreadId" // 查看线程ID
"Task.CurrentId" // 查看Task ID
5. 编辑并继续
支持的修改类型
| ✅ 可以修改 | ❌ 不能修改 |
|---|---|
| 方法体内的代码 | 方法签名 |
| if/for/while内的逻辑 | 添加/删除参数 |
| 局部变量的赋值 | 添加/删除方法 |
| LINQ表达式 | 修改泛型类型 |
使用步骤
-
命中断点,程序暂停
-
直接修改代码
-
按 F5 或点击 继续 按钮
-
修改立即生效
注意事项
void Calculate()
{
int result = 0;
for(int i = 0; i < 10; i++)
{
result += i; // 运行时修改为 result += i * 2
}
Console.WriteLine(result); // 会按新逻辑执行
}
快捷键
-
F5:继续执行(应用修改)
-
Shift+F5:停止调试(放弃修改)
-
如需禁用:工具 → 选项 → 调试 → 常规 → 启用“编辑并继续”
6. 异常设置
核心概念
分为两个阶段:
-
第一次机会异常(First Chance Exception):异常刚抛出时,还未被catch
-
第二次机会异常(Second Chance Exception):所有catch都没处理,程序即将崩溃
设置步骤
-
调试 → 窗口 → 异常设置
-
勾选 “Common Language Runtime Exceptions”
-
程序会在任何异常抛出时立即中断
捕获被吞掉的异常
try
{
DoSomething();
}
catch (Exception ex)
{
// 异常被吞掉,调试器不会自动中断
LogToFile(ex);
// 但在异常设置中勾选CLR异常后,执行到这里就会中断!
}
自定义异常中断条件
// 只中断特定类型的异常
// 在异常设置窗口中:
// 搜索 "FileNotFoundException" → 只勾选这个
// 编程方式控制中断
Debugger.Launch();
try
{
// 代码
}
catch (CustomException ex) when (Debugger.IsAttached)
{
Debugger.Break(); // 调试时自动中断
throw;
}
异常窗口显示信息
异常类型: System.NullReferenceException
消息: Object reference not set to an instance of an object.
堆栈跟踪: 显示调用链
查看详细信息: 可查看异常内部的所有属性
7. 即时窗口
打开方式
调试 → 窗口 → 即时窗口 或快捷键 Ctrl+Alt+I
常用命令
| 命令 | 简写 | 说明 |
|---|---|---|
?expression |
? |
计算表达式并显示结果 |
var = value |
- | 修改变量值 |
>cmd |
> |
执行调试器命令 |
>? command |
>? |
查看命令帮助 |
实战示例
// 假设在调试中,当前代码有变量:
string customerName = "张三";
List<int> numbers = new List<int> { 1, 2, 3 };
// 在即时窗口中输入:
? customerName.Length
// 输出: 2
? numbers.Sum()
// 输出: 6
// 修改变量值
customerName = "李四"
? customerName
// 输出: "李四"
// 调用当前上下文的方法
? GetCustomerDiscount(customerName)
// 输出: 0.15
// 执行代码块
>? numbers.ForEach(n => System.Diagnostics.Debug.WriteLine(n))
无法执行的操作
-
定义新类或新方法
-
添加引用
-
执行异步方法(需要特殊处理)
技巧:在即时窗口中调用静态方法
// 需要完全限定名
? System.IO.File.ReadAllText(@"C:\test.txt")
// 或先导入命名空间
>using System.IO;
? File.ReadAllText(@"C:\test.txt")
8. 强制调用堆栈返回
核心操作:Set Next Statement
操作方法
-
命中断点
-
打开调用堆栈窗口(调试 → 窗口 → 调用堆栈)
-
右键点击任意一行 → “将指令指针设置到此帧”
-
或直接在代码编辑器中,将黄色箭头拖拽到其他行
典型应用场景
场景1:跳过大段耗时代码
public void ProcessData()
{
// 1. 验证数据
ValidateData(); // ← 当前执行到这里
// 2. 复杂计算(耗时30秒)
var result = HeavyCalculation(); // 想跳过这个
// 3. 保存结果
SaveResult(result);
}
// 操作:将黄色箭头从HeavyCalculation()直接拖到SaveResult()后面
场景2:重新执行一段代码
public void UpdateCustomer(Customer customer)
{
// 发现customer.Name需要修改后重新执行
// 可以修改代码后,将箭头拖回方法开头
if (string.IsNullOrEmpty(customer.Name))
{
throw new ArgumentException("Name required");
}
SaveToDatabase(customer);
SendNotification(customer);
}
警告
-
⚠️ 跳过方法可能导致状态不一致
-
⚠️ 回到之前位置可能重新执行带副作用的操作(如数据库写入)
-
⚠️ 只应在纯计算或临时调试时使用
9. 内存窗口与固定变量
内存窗口
打开方式
调试运行时 → 调试 → 窗口 → 内存 → 内存1
实际使用场景
byte[] buffer = new byte[16] { 0x01, 0x02, 0x03, ... };
string text = "Hello";
int[] numbers = new int[] { 1, 2, 3, 4 };
// 在监视窗口复制buffer的内存地址
// 粘贴到内存窗口的地址栏
// 可以看到原始字节:
// 0x01 0x02 0x03 0x04 ...
固定变量(Pin)
操作方法
-
调试运行时,右键点击变量 → 固定到源
-
变量旁边出现📌图钉
-
变量值会悬浮显示,跨步骤保持
高级:数据提示的图形化
// 鼠标悬停时,对于集合类型
List<string> names = new List<string> { "张三", "李四", "王五" };
// 鼠标悬停 → 点击小表格图标 → 看到网格视图
// 可排序、筛选、搜索
监视窗口技巧
// 在监视窗口可以输入各种表达式
string.Concat("A", "B")
myArray.Length
myList[5].ToString()
((MyClass)obj).PropertyName
10. 性能提示与调试器可视化工具
数据集的快速统计
List<int> largeList = Enumerable.Range(1, 10000).ToList();
// 鼠标悬停在largeList上
// 点击右侧的小图标(柱状图)
// 直接显示:
// - Count: 10000
// - 最小值: 1
// - 最大值: 10000
// - 平均值: 5000.5
字符串可视化工具
string longText = File.ReadAllText("largefile.txt");
// 鼠标悬停 → 点击放大镜图标
// 弹出窗口显示完整字符串,支持:
// - 文本视图
// - XML视图(自动格式化)
// - JSON视图(自动格式化)
// - HTML视图(预览)
内置可视化工具类型
| 数据类型 | 可视化工具 |
|---|---|
| 字符串 | 文本/XML/JSON/HTML |
| DataSet/DataTable | 表格视图 |
| IEnumerable | 结果集视图(可查询) |
| 图像 | 缩略图预览 |
| WPF控件 | 可视化树查看器 |
自定义调试可视化工具
[DebuggerVisualizer(typeof(MyVisualizer))]
public class MyData
{
// 自定义调试时的显示方式
}
// 创建可视化工具需要:
// 1. 继承DialogDebuggerVisualizer
// 2. 实现Show方法
// 3. 注册到VS
综合调试最佳实践
快捷键速查表
| 快捷键 | 功能 |
|---|---|
| F5 | 开始调试/继续 |
| Shift+F5 | 停止调试 |
| F9 | 切换断点 |
| F10 | 逐过程 |
| F11 | 逐语句 |
| Shift+F11 | 跳出当前方法 |
| Ctrl+Shift+F5 | 重启调试 |
| Ctrl+Alt+Q | 快速监视 |
| Ctrl+Alt+I | 即时窗口 |
| Ctrl+Alt+D | 异常设置 |
调试配置技巧
<!-- appsettings.Development.json -->
{
"Logging": {
"LogLevel": {
"Default": "Debug" // 开发环境详细日志
}
},
"UseMockData": true // 调试时使用模拟数据
}
条件编译调试代码
#if DEBUG
// 仅在Debug模式下编译
Logger.EnableVerboseLogging();
UseTestDatabase();
#else
UseProductionDatabase();
#endif
[Conditional("DEBUG")]
public void LogDebug(string message)
{
Console.WriteLine($"[DEBUG] {message}");
}
使用Debug类的方法
Debug.WriteLine("执行到这里"); // 输出到输出窗口
Debug.Assert(value > 0, "value必须大于0"); // 条件失败时中断
Debug.Indent(); // 缩进输出
Debug.Unindent(); // 取消缩进
Debug.Print("Counter: {0}", counter); // 格式化输出
这些技巧配合使用,可以定位95%以上的逻辑问题。建议先从条件断点和异常设置开始,逐步掌握其他高级功能。
更多推荐
所有评论(0)