非中断(正常)模式下的调试
本书经常使用的一个命令是 Console.WriteLine()
函数,它可以把文本输出到控制台。在开发应用程序时,这个函数可以方便地获得操作的额外反馈,例如:
Console.WriteLine("MyFunc() Function about to be called");
MyFunc("Do something.");
Console.WriteLine("MyFunc() Function execution completed.");
这段代码说明了如何获取 MyFunc()
函数的额外信息。这么做完全正确,但控制台的输出结果会比较混乱。在开发其他类型的应用程序时,如桌面应用程序,没有用于输出信息的控制台。作为一种替代方法,可将文本输出到另一个位置上—IDE中的 输出
窗口。
第 2 章简要介绍了 错误列表
窗口,提到其他窗口也可以显示在这个位置上。其中一个窗口就是 输出
窗口,在调试时这个窗口非常有用。要显示这个窗口,可以选择 视图(V) | 输出(O)
。在这个窗口中,可以查看与代码的编译和执行相关的信息,包括在编译过程中遇到的错误等,还可以将自定义的诊断信息直接写到这个窗口中。该窗口 如图 7-1 所示
。
图7-1
使用输出
窗口的下拉菜单可以选择三种模式:生成
、生成顺序
和调试
。分别显示编译和运行期间的信息。本书提到的 “写入输出
窗口”时,实际上是指 “写入输出
窗口的调试
模式视图”。
另外,还可以创建一个日志文件,在运行应用程序时,会把信息添加到该日志文件中。把信息写入日志文件所用的技巧与把文本写到 输出
窗口上所用的技巧相同,但需要理解如何从 C#应用程序中访问文件系统。我们把这个功能放在后面的章节中加以讨论,因为目前不必了解文件访问技巧也可以完成许多工作。
1. 输出调试信息
在运行期间把文本写入 “输出” 窗口是非常简单的。只要用需要的调用替换 Console.WriteLine()
调用,就可以把文本写到希望的地方。此时可以使用如下两个命令:
● Debug.WriteLine()
● Trace.WriteLine()
这两个命令函数的用法几乎完全相同,但有一个重要区别:第一个命令仅在调试模式下运行,而第二个命令还可以用于发布程序。实际上,Debug.WriteLine()
命令甚至不能编译到可发布的程序中,在发布版本中,该命令会消失,这肯定有其优点(编译好的代码文件比较小)。实际上,一个源文件可以创建出两个版本的应用程序。调试版本显示所有的额外诊断信息,而发布版本没有这个开销,也不向用户显示可能导致它们反感的消息。
这两个函数的用法与 Console.WriteLine()
是不同的。其唯一的字符串参数用于输出消息,而不需要使用 {X}
语法插入变量值。这意味着必须使用 +
串联运算符等方式在字符串中插入变量值。它们还可以有第二个字符串参数(可选),用于显示输出文本的类型,这样,如果应用程序的不同地方输出了类似的消息,我们就可以马上确定 输出
窗口中显示的是哪些输出信息。
这些函数的一般输出如下所示:
<category>: <message>
例如,下面的语句把 MyFunc
作为可选的类别参数:
Debug.WriteLine("Added1 to i", "Myfunc");
其结果为:
MyFunc: Added 1 to i
下面的示例按这种方式输出调试信息。 修改代码,如下所示:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TracePoint
{
class Program
{
static void Main(string[] args)
{
int[] testArray = { 4, 7, 4, 2, 7, 3, 7, 8, 3, 9, 1, 9 };
int[] maxValIndices;
int maxVal = Maxima(testArray, out maxValIndices);
Console.WriteLine("Maximum value {0} found at element indices:",
maxVal);
foreach (int index in maxValIndices)
{
Console.WriteLine(index);
}
Console.ReadKey();
}
static int Maxima(int[] integers, out int[] indices)
{
Debug.WriteLine("Maximum value search started.");
indices = new int[1];
int maxVal = integers[0];
indices[0] = 0;
int count = 1;
Debug.WriteLine(string.Format("Maximum value initialized to {0},
at element index 0.", maxVal));
for (int i = 1; i < integers.Length; i++)
{
Debug.WriteLine(string.Format("Now looking at element at index {0}", i));
if (integers[i] > maxVal)
{
maxVal = integers[i];
count = 1;
indices = new int[1];
indices[0] = i;
Debug.WriteLine(string.Format("New maximum found. New value is {0}, at element index {1}.",
maxVal, i));
}
else
{
if (integers[i] == maxVal)
{
count ++;
int[] oldIndices = indices;
indices = new int[count];
oldIndices.CopyTo(indices, 0);
indices[count - 1] = i;
Debug.WriteLine(string.Format("Duplicate maximum found at element index {0}.", i));
}
}
}
Trace.WriteLine(string.Format("Maximum value {0} found, with {1} occurrences.",
maxVal, count));
Debug.WriteLine("Maximum value search completed.");
return maxVal;
}
}
}
在 调试
模式下执行代码,然后中断应用程序的执行,查看 输出
窗口中的内容。
使用标准工具栏的下拉菜单,切换到 Release
模式。
再次运行程序,这次在 Release
模式下运行,并在执行中止时再查看一下 输出
窗口。
示例的说明
这个应用程序是第 6 章中一个示例的扩展版本,它使用一个函数计算整数数组中的最大值。这个版本也返回一个索引数组,表示最大值在数组中的位置,以便调用代码处理这些元素。
using System.Diagnostics;
这简化了前面讨论的函数的访问,因为它们包含在 System.Diagnostics
名称空间中,没有这个 using
语句,下面的代码:
Debug.WriteLine("Bananas");
就需要进一步予以限定,重新编写这行语句,如下所示:
System.Diagnostics.Debug.WriteLine("Bananas");
using
语句使代码更简单,缩短了代码的长度。
Main()
中的代码仅初始化一个测试用的整数数组 testArray
,并声明了另一个整数数组 maxValIndices
,以存储 Maxima()
(执行计算的函数)的索引输出结果,接着调用这个函数。函数返回后,代码就会输出结果。
Maxima()
稍微复杂一些,但用到的代码大部分在前面已经看到过。在数组中进行搜索的方式与第 6 章的 MaxVal()
函数类似,但要用一个记录来存储最大值的索引。
特别需要注意用来跟踪索引的函数(而不是输出调试信息的那些代码行)。Maxima()
并没有返回一个足以存储源数组中每个索引的数组(需要与源数组有相同的维数),而是返回一个正好能容纳搜索到的索引的数组。这可以通过在搜索过程中连续重建不同长度的数组来实现。必须这么做,因为一旦创建好数组,就不能重新设置长度。
开始搜索时,假定源数组中的第一个元素就是最大值,而且数组中只有一个最大值。因此可以为 maxVal
(函数的返回值,即搜索到的最大值)和 indices
( out
参数数组,存储搜索到的最大值的索引)设置值。maxVlu
被赋予 integers
中第一个元素的值,indices
被赋予一个值 0, 即数组中第一个元素的索引。在变量 count
中存储搜索到最大值的个数,以便跟踪 indices
数组。
函数的主体是一个循环,它迭代 integers
数组中的各个值,但忽略第一个值,因为它已经处理过了这个值。每个值都与 maxVal
的当前值进行比较,如果 maxVal
更大,就忽略该值。如果当前处理的值比 maxVal
大,就修改 maxVal
和 indices
,以反映这种情况。如果当前处理的值与 maxVal
相等,就递增 count
,用一个新数组替代 indices
。这个新数组比旧 indices
数组多一个元素,包含搜索到的新索引。
最后一个功能的代码如下所示:
if (integers[i] == maxVal)
{
count++;
int[] oldIndices = indices;
indices = new int[count];
oldIndices.CopyTo(indices, 0);
indices[count - 1] = i;
Debug.WriteLine(string.Format("Duplicate maximum found at element index {0}.", i));
}
这段代码把旧 indices
数组备份到 if
代码块的 oldIndices
局部整型数组中。注意 ⚠️使用 <array>.CopyTo()
函数把 oldIndices
中的值复制到新的 indices
数组中。这个函数的参数是一个目标数组和一个用于复制第一个元素的索引,并把所有的值都粘贴到目标数组中。
在代码中,各个文本部分都使用 Debug.WriteLine()
和 Trace.WriteLine()
函数进行输出,这些函数使用 string.Format()
函数把变量值嵌套在字符串中,其方式与 Console.WriteLine()
相同。这比使用 +
串联运算符更加高效。
在 Debug
模式下运行应用程序时,其最终结果是一个完整记录,它记述了在循环中计算出结果所采取的步骤。在 Release
模式下,仅能看到计算的最终结果,因为没有调用 Debug.WriteLine()
函数。
除了这些 WriteLine()
函数外,还需要注意其他一些函数。首先是 Console.Write()
的等价函数:
● Debug.Write()
● Trace.Write()
这两个函数使用的语法与 WriteLine()
函数相同(一个或两个参数,即一个消息和可选的类型),但它们是有区别的,因为它们没有添加行尾字符。
还有下列函数:
● Debug.WriteLineIf()
● Trace.WriteLineIf()
● Debug.WriteIf()
● Trace.WriteIf()
这些函数的参数都与没有 if
的对应函数相同,但增加了一个必选参数,且该参数放在参数列表的最前面。这个参数的值为布尔值(或者计算结果为布尔值的表达式),只有这个值为 true
时,函数才会输出文本。使用这些函数可以根据条件把文本输出到 输出
窗口中。
例如,只需在某些情况下输出调试信息,所以代码中有许多 Debug.WriteLineIf()
语句,在满足特定的条件后它们才会输出信息。如果没有满足条件,就不显示它们,以防 输出
窗口显示多余的信息。
2. 跟踪点
另一种把信息输出到 输出
窗口中的方法是使用跟踪点( tracepoint
)。这是VS的一个功能,而不是 C#的功能,但其作用与使用 Debug.WriteLine()
相同。它实际上是输出调试信息且不修改代码的一种方式。
为了演示跟踪点,可用它们替代上一个示例中的调试命令。添加跟踪点的过程如下:
(1)把光标放在要插入跟踪点的代码行上。跟踪点会在执行这行代码之前被处理。
(2)右击该行代码,选择 断点
| 插入跟踪点
。
(3)在打开的 命中断点时
对话框时,在 打印消息
文本框中键入要输出的字符串。如果要输出变量值,应把变量名放在花括号中。
(4)单击 OK
按钮。在包含跟踪点的代码行左边会出现一个红色的菱形,该行代码也会突出显示为红色。
看一下添加跟踪点的对话框标题和所需要的菜单选项,显然,跟踪点是断点的一种形式(可以暂停应用程序的执行,就像断点一样)。断点一般用于更高级的调试目的,本章稍后将介绍断点。
图7-4
显示了 TracePoints中第 31 行所需的跟踪点。在删除已有的 Debug.WriteLine()
语句后,对代码进行编号。
图7-4
如图 7-4
中的文本所示,跟踪点允许插入与跟踪点的位置和上下文相关的其他有用信息。用户应试着使用这些值,尤其是 $FUNCTION 和 $CALLER,看看可以得到什么额外信息。还可以看出,跟踪点可以执行宏,但这是一项高级功能,这里不予以介绍。
还有一个窗口可用于快速查看应用程序中的跟踪点。要显示这个窗口,可从VS菜单中选择 调试(D)
| 窗口(W)
| 断点(B)
。这是显示断点的通用窗口(如前所述,跟踪点是断点的一种形式)。可以定制显示的内容,从这个窗口的 列
下拉框中添加 命中条件
列,显示与跟踪点关系更密切的信息。图 7-5
显示的窗口配置了这个列,还显示了添加到 TracePoint
中的所有跟踪点。
在调试模式下执行这个应用程序,会得到与前面完全相同的结果。在代码窗口中右击跟踪点,或者利用 断点
窗口,可以删除或临时禁用跟踪点。在 Breakpoints
窗口中,跟踪点左边的复选框指示是否启用跟踪点;禁用的跟踪点未被选中,在代码窗口中显示为菱形框,而不是实心菱形。
3. 诊断输出与跟踪点
前面介绍了两种输出相同信息的方法,下面看看它们的优缺点。首先,跟踪点与 Trace
命令并不等价,也就是说,不能使用跟踪点在发布版本中输出信息。这是因为跟踪点并没有包含在应用程序中。跟踪点由VS处理,在应用程序的已编译版本中,跟踪点是不存在的。只有应用程序运行在VS调试器中时,跟踪点才起作用。
跟踪点的主要缺点也是其主要优点,即它们存储在VS中,因此可以在需要时便捷地添加到应用程序中,而且也非常容易删除。如果输出非常复杂的信息字符串,觉得跟踪点非常讨厌,只需单击表示其位置的红色菱形,就可以删除跟踪点。
跟踪点的一个优点是允许方便地添加额外信息,如上一节提到的 &FUNCTION。这个信息可以用 Debug
和 Trace
命令来编写,但比较难。总之,输出调试信息的两种方法是:
● 诊断输出: 总是要从应用程序中输出调试结果时使用这种方法,尤其是在要输出的字符串比较复杂,涉及几个变量或许多信息的情况下,使用该方法比较好。另外,如果要在执行发布版本的应用程序的过程中进行输出,Trace
命令经常是唯一的选择。
● 跟踪点: 调试应用程序时,如果希望快速输出重要信息,以便消除语义错误,应使用跟踪点。