# VisualStudio的调试

Visual Studio中有两种方式执行应用程序:调试模式和非调试模式。VisualStudio默认是在调试模式下执行的,按F5或单击工具栏中的绿色三角按钮就是在调试模式下执行应用;要在非调试模式下执行应用程序,应该按Ctrl+F5或者选择Debug | Start Without Debugging。

Visual Studio允许在两种配置下生成应用:调试(默认)和发布。工具栏中的配置管理器可在两种模式下切换。在调试模式下发布应用,应用会包含调试用的符号信息,让IDE知道每行代码执行时发生了什么,符号信息意味着跟踪变量名函数名等等,此类信息包含在.pdb文件中,这些文件位于Debug目录下。而发布版本则不需要调试版本所有的符号信息,运行速度较快。

Visual Studio的调试分两种情况,一种是在程序运行时中断程序的执行,进行调试。另一种是正常运行,打印变量的非中断模式。

# 非中断模式下的调试

在C#中的控制台应用中,最常用的就是Console.WriteLine()函数,它可以把信息输出到控制台。但是控制台的输出结果可能比较混乱,另外在开发其他类型的应用时,比如桌面应用程序,也没有用于输出信息的控制台。作为一种替代方法,可以输出到Visual Studio中的Output窗口。Output窗口有Debug,Build,Build Order模式,这些模式分别显示调试和运行期间的信息,我们平时用的就是Debug模式。

# 输出调试信息

有两条命令可以将信息输出到Output窗口:

using System.Diagnostics;

Debug.WriteLine()
Trace.WriteLine()
1
2
3
4

这两个命令函数几乎完全相同,但有一个重要区别:第一条命令仅在调试模式下运行,第二条命令可用于发布程序。Debug.WriteLine()不能编译到可发布的程序中,在发布版本中,该命令会消失。注意这两个函数同Console.WriteLine的用法是不同的,他们可以有第二个字符串参数,用于显示输出文本的类别。

class Program
{
    static void Main()
    {
        string str = "world";
        Console.WriteLine($"hello {str}");
        Trace.WriteLine($"Trace hello{str}","SelfDefine");
        Debug.WriteLine($"Debug hello{str}","SelfDefine");
        Console.ReadKey();
    }
}
1
2
3
4
5
6
7
8
9
10
11

上面的程序在工具栏的配置管理器中Debug和Release模式下分别运行,就会发现在Release模式下运行不会有Debug.WriteLine执行。

当提示无法查找或打开PDB文件时,需要:

  1. 【工具】->【选项】->【调试】->【常规】勾选“启用源服务器支持”。
  2. 【工具】->【选项】->【调试】->【符号】勾选“Microsoft符号服务器”。

# 跟踪点

另一种把信息输出到Output窗口的方法是使用跟踪点(tracepoint)。这是Visual Studio的功能,其作用域Debug.WriteLine()相同,它实际上是不修改代码输出信息的一种方式。

跟踪点是断点的一种形式,可以通过点击要停顿的行号左侧来设置跟踪点,要查看设置的所有跟踪点可以选择 Debug | Windows | Breakpoints,在这个窗口可以勾选左边的复选框来启用禁用跟踪点。

TracePoint

跟踪点可以设置条件,动作。

条件可以设置为:

  • 条件表达式:在条件表达式为真时触发动作,或者在值改变时触发动作。
  • 命中次数:在该行执行几次时触发动作。
  • 筛选器:
    • MachineName == "name"
    • ProcessId == value
    • ProcessName == "name"
    • ThreadId == value
    • ThreadName == "name"

动作可以设置为:

  • 输出一条信息,如果要输出变量,可以使用括号括起来,还有几个内部变量:

    关键字 显式内容
    $ADDRESS 当前指令
    $CALLER 调用函数名
    $CALLSTACK 调用堆栈
    $FUNCTION 当前函数名
    $PID 进程ID
    $PNAME 进程名
    $TID 线程ID
    $TNAME 线程名
    $TICK 滴答计数,来自Windows GetTickCount
  • 是否在此处跟踪点暂停

# 诊断输出与跟踪点

这两种方法各有优缺点,它们有各自的应用场景:

  • 诊断输出:如果要从应用程序中输出调试结果,尤其是输出的字符串比较复杂,涉及几个变量或许多信息的情况下,使用该方法比较合适。要在执行发布版本的应用程序的过程中进行输出,Trace命令经常是唯一的选择。
  • 跟踪点:跟踪点存储在Visual Studio中,可以在需要时添加到应用程序中,比较容易删除,它允许方便地添加额外信息,比如输出函数名,进程名等。但是到发布的应用程序中,就没有跟踪点什么事了。

# 中断模式下的调试

# 进入中断

像前面设置跟踪点一样设置断点,然后在调试模式下运行就会进入调试模式,在该模式下可以选择一条一条语句的执行,或像前面跟踪点一样有条件的执行。逐语句执行按F11,逐过程执行按F10,它们的区别是在遇到函数时,逐过程会执行完整个函数,而逐语句则会进入函数逐条执行。

此外还可以通过判定语句进入中断,判定语句常用于测试程序,它们是

Debug.Assert()
Trace.Assert()
1
2

同样,Debug常用于调试程序,而Trace可用于发布程序。这两个Assert函数都需要三个参数,第一个是判定语句,第二个,第三个是在判定语句为False时显示的短消息和长消息。

# 调试

可以在各个监视窗口查看运行过程中的变量值,其中:

  • 自动窗口是当前和前面的语句使用的变量。
  • 局部变量窗口是作用域内的所有变量。
  • 监视窗口是自己手动输入监视的变量值。

除此之外还有几个功能性窗口:

  • Immediate窗口可以计算表达式。
  • Command窗口可以输入一些命令,输入immed就可以进入Immediate窗口。
  • CallStack描述了程序是如何执行到当前位置的,显示了当前函数,调用它的函数以及调用该函数的函数等等。。。

# 错误处理

前面提到了如何查找和改正错误,使这些错误不会在代码中出现,但是有时我们知道可能会有错误发生,我们能预料到错误发生,编写足够强壮的代码以处理这些错误,而不必中断程序的执行。这个时候我们就会用到异常。

C#包含结构化异常处理(Structured Exception Handling,SEH)的语法。其语法如下:

try{
    //code that throw exception!
}
catch (<exceptionType> e) when (filterIsTrue)
{
    //deal exception!
}
finally
{
    //do it when catch is over or try is over!
}
1
2
3
4
5
6
7
8
9
10
11

说明:

  • try块包含抛出异常的代码,catch包含抛出异常时要执行的代码,finally包含始终会执行的代码,如果没有产生异常,则在try之后执行,如果产生了异常,在catch块后执行。
  • catch块中的参数时异常类型,会根据这个类型来执行catch块,如果catch没有参数,那么这个catch块会响应所有的异常。C# 6引入了"异常过滤",即在catch参数后面加when关键字,这样根据类型找到catch块后还要判定这个过滤表达式,如果为真才执行catch块。
  • SEH结构是可以嵌套的,异常会一层层往上找相应的catch块。

样例:

static void Main()
{
    string[] colorSet = { "red", "green", "yellow" };
    try
    {
        throw new System.Exception();
    }
    catch
    {
        Console.WriteLine("This is common exception handler!");
    }
    finally
    {
        Console.WriteLine("Exec finally code!");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

当同一个异常有不同的过滤时,注意如果有过滤条件的放在没有过滤条件的catch块下面就会有编译错误。

static void Main()
{
    bool enterFilter = true;
    string[] colorSet = { "red", "green", "yellow" };
    try
    {
        colorSet[3] = "black";
    }
    catch (System.IndexOutOfRangeException e) when (enterFilter)
    {
        Console.WriteLine("This is index out of range exception handler with filter!");
    }
    catch (System.IndexOutOfRangeException e)
    {
        Console.WriteLine("This is index out of range exception handler without filter!");
    }
    finally
    {
        Console.WriteLine("Exec finally code!");
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

异常嵌套执行时:

static void Main()
{
    bool enterFilter = true;
    string[] colorSet = { "red", "green", "yellow" };
    try
    {
        try
        {
            colorSet[3] = "black";
        }catch(System.IndexOutOfRangeException e)
        {
            Console.WriteLine("Index out of range!nested catch!");
            throw;
        }
        finally
        {
            Console.WriteLine("nested finally!");
        }
    }
    catch (System.IndexOutOfRangeException e) when (enterFilter)
    {
        Console.WriteLine("This is index out of range exception handler with filter!");
    }
    catch (System.IndexOutOfRangeException e)
    {
        Console.WriteLine("This is index out of range exception handler without filter!");
    }
    finally
    {
        Console.WriteLine("Exec finally code!");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

注意,如果没有嵌套里面try块的throw语句,在执行完嵌套的catch之后就不会执行外层的catch了。

# Throw表达式

前面的throw适用于已经发生操作的语句中,在表达式中也可以使用throw,比如:

static void Main()
{
    string name = null;
    string name2 = "hi";
    string result = null;
    try
    {
        result=name ?? throw new ArgumentNullException(paramName: nameof(name), message: "It is null");
    }
    catch
    {
        Console.WriteLine("In common throw deal block!");
    }
    finally
    {
        Console.WriteLine("In finally block");
        Console.WriteLine(result);
    }
    Console.ReadKey();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

上面的双问号,称为空值合并操作符(null-coalescing operator),检查所赋的值是否为null,如果为null则抛出异常,否则继续执行。

# 列出和配置异常

.Net Framework包含许多异常类型,可以在代码中自由抛出和处理这些类型的异常。IDE提供了一个对话框,可以检查和编辑可用的异常。使用Debug | Windows | Exceptions Settings菜单项打开对话框。

在该对话框中,展开Common Language Runtime Exception的加号,就可以看到System名称空间中的异常,每个异常都可以使用异常类型旁边的复选框来配置,也可以单击上面加号来添加自定义的异常。