# 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()
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();
}
}
2
3
4
5
6
7
8
9
10
11
上面的程序在工具栏的配置管理器中Debug和Release模式下分别运行,就会发现在Release模式下运行不会有Debug.WriteLine执行。
当提示无法查找或打开PDB文件时,需要:
- 【工具】->【选项】->【调试】->【常规】勾选“启用源服务器支持”。
- 【工具】->【选项】->【调试】->【符号】勾选“Microsoft符号服务器”。
# 跟踪点
另一种把信息输出到Output窗口的方法是使用跟踪点(tracepoint)。这是Visual Studio的功能,其作用域Debug.WriteLine()相同,它实际上是不修改代码输出信息的一种方式。
跟踪点是断点的一种形式,可以通过点击要停顿的行号左侧来设置跟踪点,要查看设置的所有跟踪点可以选择 Debug | Windows | Breakpoints,在这个窗口可以勾选左边的复选框来启用禁用跟踪点。
跟踪点可以设置条件,动作。
条件可以设置为:
- 条件表达式:在条件表达式为真时触发动作,或者在值改变时触发动作。
- 命中次数:在该行执行几次时触发动作。
- 筛选器:
- 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()
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!
}
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!");
}
}
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!");
}
}
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!");
}
}
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();
}
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名称空间中的异常,每个异常都可以使用异常类型旁边的复选框来配置,也可以单击上面加号来添加自定义的异常。