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)

与普通断点的区别

  • 普通断点:程序暂停执行

  • 跟踪点:程序继续执行,只输出日志

如何设置

  1. 右键断点红点 → 选择“操作”

  2. 勾选 “将消息输出到输出窗口”

  3. 输入要输出的内容

特殊占位符

占位符 含义 示例输出
$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表达式 修改泛型类型

使用步骤

  1. 命中断点,程序暂停

  2. 直接修改代码

  3. 按 F5 或点击 继续 按钮

  4. 修改立即生效

注意事项

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都没处理,程序即将崩溃

设置步骤

  1. 调试 → 窗口 → 异常设置

  2. 勾选 “Common Language Runtime Exceptions”

  3. 程序会在任何异常抛出时立即中断

捕获被吞掉的异常

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. 命中断点

  2. 打开调用堆栈窗口(调试 → 窗口 → 调用堆栈

  3. 右键点击任意一行 → “将指令指针设置到此帧”

  4. 或直接在代码编辑器中,将黄色箭头拖拽到其他行

典型应用场景

场景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)

操作方法
  1. 调试运行时,右键点击变量 → 固定到源

  2. 变量旁边出现📌图钉

  3. 变量值会悬浮显示,跨步骤保持

高级:数据提示的图形化
// 鼠标悬停时,对于集合类型
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%以上的逻辑问题。建议先从条件断点异常设置开始,逐步掌握其他高级功能。

更多推荐