现在是时候实践你新获得的调试知识了。 这是你工作的第一天,现在可以通过修复公司旗舰产品(斐波那契计算器)中的 bug 来施展 .NET 调试技能了。
创建示例 .NET 项目以进行调试
若要设置 Visual Studio Code 以进行 .NET 调试,首先需要一个 .NET 项目。 Visual Studio Code 包含一个集成终端,这使创建新项目变得简单。
在 Visual Studio Code 中,选择“文件”>“打开文件夹”。
在选择的位置中创建名为 DotNetDebugging
的新文件夹。 然后选择“选择文件夹”。
从主菜单中选择“视图”>“终端”,以便从 Visual Studio Code 中打开集成终端。
在终端窗口中,复制粘贴以下命令:
dotnet new console
此命令会在文件夹中创建一个 Program.cs 文件(内附已编写的基本“Hello World”程序)。 它还将创建一个名为 DotNetDebugging.csproj 的 C# 项目文件。
在终端窗口中,复制粘贴以下命令来运行“Hello World”程序。
dotnet run
终端窗口显示“Hello World!”作为输出。
设置 Visual Studio Code 以进行 .NET 调试
选择 Program.cs 以打开它。
首次在 Visual Studio Code 中打开 C# 文件时,你将收到一条提示,提示你安装推荐的 C# 扩展。 如果看到此提示,请选择提示中的“安装”按钮。
Visual Studio Code 将安装 C# 扩展,并将显示另一条提示,提示你添加所需资产来生成和调试项目。 选择“是”按钮。
可以关闭“扩展:C#”选项卡,重点关注我们调试的代码。
添加斐波那契程序逻辑
我们的当前项目向控制台编写了“Hello World”消息,并没有太多内容需要调试。 相反,你将使用简短的 .NET 程序来计算第 N 号斐波那契数列。
斐波纳契数列是一组以数字 0 和 1 开头的数字,后面的每个数字都是前两个数字的和。 序列以此类推,如下所示:
0, 1, 1, 2, 3, 5, 8, 13, 21...
选择 Program.cs 以打开它。
将 Program.cs 的内容替换为以下代码:
int result = Fibonacci(5);
Console.WriteLine(result);
static int Fibonacci(int n)
{
int n1 = 0;
int n2 = 1;
int sum;
for (int i = 2; i < n; i++)
{
sum = n1 + n2;
n1 = n2;
n2 = sum;
}
return n == 0 ? n1 : n2;
}
备注
此代码包含错误,我们稍后将在本模块中进行调试。 在我们修复该错误之前,我们建议你不要在任何任务关键的斐波那契应用程序中使用它。
对于 Windows 和 Linux,通过选择“Ctrl+S”来保存该文件。 对于 Mac,请选择“Cmd+S”。
让我们来看看已更新的代码在调试之前是如何工作的。 通过在终端输入以下命令来运行程序:
dotnet run
终端输出中显示结果为 3。 斐波那契序列图显示了括号中每个值从零开始的序列位置,查阅该图时,你会看到结果应为 5。 现在可以熟悉调试器并修复此程序了。
0 (0), 1 (1), 1 (2), 2 (3), 3 (4), 5 (5), 8 (6), 13 (7), 21 (8)...
分析问题
通过选择“运行”选项卡并选择“开始调试”按钮启动程序。
应看到程序快速完成。 这是正常的,因为尚未添加任何断点。
对于 Windows 和 Linux,如果调试控制台未出现,则选择“Ctrl+Shift+Y”。 对于 Mac,请选择“Cmd+Shift+Y”。 应看到几行诊断信息,后跟下面的几行内容:
...
Loaded 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\6.0.0\System.Threading.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Loaded 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\6.0.0\System.Text.Encoding.Extensions.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
3
The program '[88820] DotNetDebugging.dll' has exited with code 0 (0x0).
顶部的行指示默认调试设置启用“仅我的代码”选项。 这意味着,调试器将仅调试你的代码,除非禁用此模式,否则不会单步执行 .NET 的源代码。 使用此选项时,你便可以专注于调试代码。
在调试控制台输出的末尾,你会看到程序将 3 写入控制台,并存在代码 0。 通常,程序退出代码 0 表示程序已运行并退出且不崩溃。 但是,崩溃和返回正确的值之间存在差异。 在这种情况下,我们要求程序计算斐波那契数列的第 5 个值:
0 (0), 1 (1), 1 (2), 2 (3), 3 (4), 5 (5), 8 (6), 13 (7), 21 (8)...
此列表中的第 5 个值为 5,但我们的程序返回了 3。 让我们使用调试器来诊断和解决此错误。
使用断点并逐步执行
通过单击 int result = Fibonacci(5);
上第 1 行的左边距来添加断点。
再次开始调试。 程序开始执行。 由于你设置了断点,因此程序会在第 1 行中断(暂停执行)。 使用调试器控件单步执行 Fibonacci()
函数。
检查变量状态
现在,使用“变量”面板检查不同变量的值。
- 为
n
参数显示的值是什么? - 函数执行开始时,局部变量
n1
、n2
和sum
的值分别是什么?
接下来,我们将使用“单步跳过”调试器控件前进到 for
循环。
在所读取的行上继续前进,直至到达 for
循环内的第一行:
sum = n1 + n2;
备注
你可能已注意到,需要在命令中执行多个步骤才能越过
for(...) {}
行。 出现这种情况的原因是此行上存在多个语句。 单步执行时,将移动到代码中的下一个语句。 通常,每行只有一个语句。 但如果不是这样,则需要执行多个步骤才能转到下一行。
思考代码
调试的一个重要部分是,停下来并对你认为代码的某些部分(函数和块,如循环)正在尝试执行的操作做出一些明智的猜想。 你不确定也没关系,这就是调试过程的一部分。 但主动参与调试过程会有助于更快地找到 bug。
在深入了解之前,让我们记住斐波纳契数列是一组以数字 0 和 1 开头的数字,后面的每个数字都是前两个数字的和。
这意味着:
Fibonacci(0) = 0
Fibonacci(1) = 1
Fibonacci(2) = 1 (0 + 1)
Fibonacci(3) = 2 (1 + 1)
Fibonacci(4) = 3 (1 + 2)
Fibonacci(5) = 5 (2 + 3)
了解该定义并查看此 for
循环,我们可以推断出:
- 该循环从 2 计数到
n
(我们要查找的斐波纳契数列号)。 - 如果
n
小于 2,则循环将永不运行。 如果n
为 0,则函数末尾的return
语句将返回 0;如果n
为 1 或 2,则返回 1。 根据定义,这是斐波纳契数列中的第 0 个、第 1 个和第 2 个值。 - 更有趣的情况是当
n
大于 2 时。 在这些情况下,当前值定义为前两个值的和。 因此,对于此循环,n1
和n2
是前两个值,sum
是当前迭代的值。 因此,每次计算出前两个值的和并将其设置为sum
时,我们都会更新n1
和n2
值。
好了,除此之外,我们不需要过多考虑。 我们可以依靠调试器完成一些操作。 但有必要考虑一下代码,看看它是否会执行预期的操作,如果未执行,则需要获取更多最新信息。
使用断点找到 bug
单步执行代码可能非常有用,但却很繁琐。 使用循环或其他重复调用的代码时尤为如此。 我们可以在循环的第一行上设置新断点,而不是反复单步执行循环。
当我们执行此操作时,使用策略放置断点非常重要。 我们对 sum
特别感兴趣,因为它表示当前最大斐波那契值。 因此,让我们在设置 sum
后在行上放置断点。
在第 13 行上添加第二个断点。
备注
如果你注意到继续运行代码,然后单步执行一行或两行代码,则可以轻松地将断点更新为更高效的行。
现在,在循环中设置好断点后,使用“继续”调试器控件前进,直至到达该断点。 查看本地变量,将看到以下几行内容:
n [int]: 5
n1 [int]: 0
n2 [int]: 1
sum [int]: 1
i [int]: 2
这些行看似都正确。 第一次单步执行循环时,前两个值的 sum
为 1。 我们可以利用断点,从而在下次单步执行循环时跳至该断点,而不是逐行单步执行。
选择“继续”以继续执行程序流,直至到达下一个断点,该断点将在下一次循环中出现。
备注
使用“继续”时,无需担心跳过 bug。 你应该预料到,往往要多次调试代码才能找到问题。 相比非常小心谨慎地进行单步执行,多次运行代码往往速度更快。这一次,我们看到了以下值:
n [int]: 5
n1 [int]: 1
n2 [int]: 1
sum [int]: 2
i [int]: 3
让我们思考一下。 这些值是否仍有意义? 看起来像是有。 对于第三个斐波纳契数,我们预计将看到 sum
等于 2,实际上的确如此。
好了,让我们选择“继续”再次循环。
n [int]: 5
n1 [int]: 1
n2 [int]: 2
sum [int]: 3
i [int]: 4
同样,一切看起来都很好。 数列中的第 4 个值应为 3。
此时,你可能会想知道此代码是否一直是正确的,并且想象一下 bug! 让我们在循环中最后一次继续这样做。 再次选择“继续”。等待一分钟。 程序已完成运行并打印出 3! 这不正确。好了,不要担心。 我们并没有失败,而是了解了情况。 现在,我们知道代码会一直正确运行循环,直到 i
等于 4,但随后会在计算最终值之前退出。 我开始对 bug 的位置有所了解,你呢?
让我们在第 17 行上再设置一个断点,显示如下:
return n == 0 ? n1 : n2;
通过此断点,我们可以在函数退出前检查程序状态。 我们已了解到我们有望从第 1 行和第 13 行上的先前断点获得的所有内容,因此可以将其清除。
删除第 1 行和第 13 行上的先前断点。 为此,可以在行号旁边的空白处单击它们,或者在左下角的“断点”窗格中清除第 1 行和第 13 行的断点复选框。
现在,我们能够更好地了解发生的情况,并设置旨在捕获行为异常时的程序的断点,我们应该能够捕获此 bug!
最后一次启动调试器。
n [int]: 5
n1 [int]: 2
n2 [int]: 3
sum [int]: 3
这不是正确的。 我们特别要求提供 Fibonacci(5),而我们得到的是 Fibonacci(4)。 此函数返回 n2
,每个循环迭代计算 sum
值并将 n2
设置为等于 sum
。根据此信息以及以前的调试运行,我们可以看到,该循环在 i
为 4(而不是 5)时退出。让我们再仔细看看 for
循环的第一行。
for (int i = 2; i < n; i++)
好了,请等待一分钟! 这意味着,一旦 for 循环的顶部看到 i
不再小于 n
,它将立即退出。 这表示,如果 i
等于 n
,循环代码将无法运行。 我们想要的似乎是在 i <= n
之前运行,而不是:
for (int i = 2; i <= n; i++)
因此进行上述更改后,更新的程序应类似以下示例:
int result = Fibonacci(5);
Console.WriteLine(result);
static int Fibonacci(int n)
{
int n1 = 0;
int n2 = 1;
int sum;
for (int i = 2; i <= n; i++)
{
sum = n1 + n2;
n1 = n2;
n2 = sum;
}
return n == 0 ? n1 : n2;
}
停止调试会话(如果尚未这样做)。
接下来,对第 10 行进行前面的更改,并将断点留在第 17 行上。
重启调试程序。 这一次,当我们操作到第 17 行上的断点时,将看到以下值:
n [int]: 5
n1 [int]: 3
n2 [int]: 5
sum [int]: 5
喂! 看起来我们做到了! 很好,你已保挽救了 Fibonacci, Inc. 的一天!
选择“继续”,只是为了确保程序返回正确的值。
5
The program '[105260] DotNetDebugging.dll' has exited with code 0 (0x0).
这样会返回正确的输出。
你成功了! 你使用 Visual Studio Code 中的 .NET 调试器调试了你并未编写的某些代码。
在下一单元中,你将了解如何使用内置于 .NET 中的日志记录和跟踪功能,使编写的代码更易于调试。