练习 - 使用 Visual Studio Code 进行调试

已完成

现在是时候实践你新获得的调试知识了。 这是你工作的第一天,现在可以通过修复公司旗舰产品(斐波那契计算器)中的 bug 来施展 .NET 调试技能了。

创建示例 .NET 项目以进行调试

若要设置 Visual Studio Code 以进行 .NET 调试,首先需要一个 .NET 项目。 Visual Studio Code 包含一个集成终端,这使创建新项目变得简单。

  1. 在 Visual Studio Code 中,选择“文件”“打开文件夹”。

  2. 在选择的位置中创建名为 DotNetDebugging 的新文件夹。 然后选择“选择文件夹”

  3. 从主菜单中选择“视图”“终端”,以便从 Visual Studio Code 中打开集成终端。

  4. 在终端窗口中,复制粘贴以下命令:

    dotnet new console
    

    此命令会在文件夹中创建一个 Program.cs 文件(内附已编写的基本“Hello World”程序)。 它还将创建一个名为 DotNetDebugging.csproj 的 C# 项目文件。

  5. 在终端窗口中,复制粘贴以下命令来运行“Hello World”程序。

    dotnet run
    

    终端窗口显示“Hello World!”作为输出。

设置 Visual Studio Code 以进行 .NET 调试

  1. 选择 Program.cs 以打开它。

  2. 首次在 Visual Studio Code 中打开 C# 文件时,你将收到一条提示,提示你安装推荐的 C# 扩展。 如果看到此提示,请选择提示中的“安装”按钮。

    Visual Studio Code 提示安装 C# 扩展的屏幕截图。

  3. Visual Studio Code 将安装 C# 扩展,并将显示另一条提示,提示你添加所需资产来生成和调试项目。 选择“是”按钮。

    Visual Studio Code 提示添加生成和调试 .NET 项目所需的资产的屏幕截图。

  4. 可以关闭“扩展:C#”选项卡,重点关注我们调试的代码。

添加斐波那契程序逻辑

我们的当前项目向控制台编写了“Hello World”消息,并没有太多内容需要调试。 相反,你将使用简短的 .NET 程序来计算第 N 号斐波那契数列。

斐波纳契数列是一组以数字 0 和 1 开头的数字,后面的每个数字都是前两个数字的和。 序列以此类推,如下所示:

0, 1, 1, 2, 3, 5, 8, 13, 21...
  1. 选择 Program.cs 以打开它。

  2. 将 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;
    }
    

    备注

    此代码包含错误,我们稍后将在本模块中进行调试。 在我们修复该错误之前,我们建议你不要在任何任务关键的斐波那契应用程序中使用它。

  3. 对于 Windows 和 Linux,通过选择“Ctrl+S”来保存该文件。 对于 Mac,请选择“Cmd+S”。

  4. 我们来看一下已更新的代码在调试之前是如何工作的。 通过在终端输入以下命令来运行程序:

    dotnet run
    

    包含已修改的程序输出的终端窗口。

  5. 终端输出中显示结果为 3。 斐波那契序列图显示了括号中每个值从零开始的序列位置,查阅该图时,你会看到结果应为 5。 现在可以熟悉调试器并修复此程序了。

    0 (0), 1 (1), 1 (2), 2 (3), 3 (4), 5 (5), 8 (6), 13 (7), 21 (8)...
    

分析问题

  1. 通过选择左侧的“运行和调试”选项卡,然后选择“开始调试”按钮来启动程序。 可能需要先选择“运行和调试”按钮,然后再选择 Program.cs 文件

    Visual Studio Code 中“开始调试”按钮的屏幕截图。

    应看到程序快速完成。 这是正常的,因为尚未添加任何断点。

  2. 对于 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。 让我们使用调试器来诊断和解决此错误。

使用断点并逐步执行

  1. 通过单击 上第 1 行的左边距来添加断点。

    代码中断点位置的屏幕截图。

  2. 再次开始调试。 程序开始执行。 由于你设置了断点,因此程序会在第 1 行中断(暂停执行)。 使用调试器控件单步执行 Fibonacci() 函数。

    “单步执行”按钮的屏幕截图。

检查变量状态

现在,使用“变量”面板检查不同变量的值。

“变量”面板的屏幕截图。

  • n 参数显示的值是什么?
  • 函数执行开始时,局部变量 n1n2sum 的值分别是什么?
  1. 接下来,我们将使用“单步跳过”调试器控件前进到 for 循环。

    “单步跳过”按钮的屏幕截图。

  2. 在所读取的行上继续前进,直至到达 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 循环,我们可以推断出:

  1. 该循环从 2 计数到 n(我们要查找的斐波纳契数列号)。
  2. 如果 n 小于 2,则循环将永不运行。 如果 return 为 0,则函数末尾的 n 语句将返回 0;如果 n 为 1 或 2,则返回 1。 根据定义,这是斐波纳契数列中的第 0 个、第 1 个和第 2 个值。
  3. 更有趣的情况是当 n 大于 2 时。 在这些情况下,当前值定义为前两个值的和。 因此,对于此循环,n1n2 是前两个值,sum 是当前迭代的值。 因此,每次计算出前两个值的和并将其设置为 sum 时,我们都会更新 n1n2 值。

好了,除此之外,我们不需要过多考虑。 我们可以依靠调试器完成一些操作。 但有必要考虑一下代码,看看它是否会执行预期的操作,如果未执行,则需要获取更多最新信息。

使用断点找到 bug

单步执行代码可能会很有帮助,但可能会很繁琐,尤其是在使用循环或重复调用其他代码时。 我们可以在循环的第一行上设置新断点,而不是反复单步执行循环。

当我们执行此操作时,使用策略放置断点非常重要。 我们对 sum 的值特别感兴趣,因为它表示当前最大 Fibonacci 值。 因此,让我们在设置 sum在行上放置断点。

  1. 在第 13 行上添加第二个断点。

    显示要设置的第二个断点的屏幕截图。

    备注

    如果你注意到继续运行代码,然后单步执行一行或两行代码,则可以轻松地将断点更新为更高效的行。

  2. 现在,在循环中设置好断点后,使用“继续”调试器控件前进,直至到达该断点。 查看本地变量,将看到以下几行内容:

    n [int]: 5
    n1 [int]: 0
    n2 [int]: 1
    sum [int]: 1
    i [int]: 2
    

    这些行看似都正确。 第一次单步执行循环时,前两个值的 sum 为 1。 我们可以利用断点,从而在下次单步执行循环时跳至该断点,而不是逐行单步执行。

  3. 选择“继续”以继续执行程序流,直至到达下一个断点,该断点将在下一次循环中出现。

    备注

    使用“继续”时,无需担心跳过 bug。 你应该预料到,往往要多次调试代码才能找到问题。 相比非常小心谨慎地进行单步执行,多次运行代码往往速度更快。

    这一次,我们看到了以下值:

    n [int]: 5
    n1 [int]: 1
    n2 [int]: 1
    sum [int]: 2
    i [int]: 3
    

    让我们思考一下。 这些值是否仍有意义? 看起来像是有。 对于第三个斐波纳契数,我们预计将看到 sum 等于 2,实际上的确如此。

  4. 好了,让我们选择“继续”再次循环。

    n [int]: 5
    n1 [int]: 1
    n2 [int]: 2
    sum [int]: 3
    i [int]: 4
    

    同样,一切看起来都很好。 数列中的第 4 个值应为 3。

  5. 此时,你可能会想知道此代码是否一直是正确的,并且想象一下 bug! 让我们在循环中最后一次继续这样做。 再次选择“继续”。

    等待一分钟。 程序已完成运行并打印出 3! 这不正确。

    好了,不要担心。 我们并没有失败,而是了解了情况。 现在,我们知道代码会一直正确运行循环,直到 i 等于 4,但随后会在计算最终值之前退出。 我开始对 bug 的位置有所了解。 你呢?

  6. 让我们在第 17 行上再设置一个断点,显示如下:

    return n == 0 ? n1 : n2;
    

    通过此断点,我们可以在函数退出前检查程序状态。 我们已了解到我们有望从第 1 行和第 13 行上的先前断点获得的所有内容,因此可以将其清除。

  7. 移除第 1 行和第 13 行上的先前断点。 为此,可以在行号旁边的空白处单击它们,或者在左下角的“断点”窗格中清除第 1 行和第 13 行的断点复选框

    显示“断点”窗格中列出的断点的屏幕截图。

    现在,我们能够更好地了解发生的情况,并设置旨在捕获行为异常时的程序的断点,我们应该能够捕获此 bug!

  8. 最后一次启动调试器。

    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;
    }
    
  9. 停止调试会话(如果尚未这样做)。

  10. 接下来,对第 10 行进行前面的更改,并保留第 17 行上的断点。

  11. 重启调试程序。 这一次,当我们操作到第 17 行上的断点时,将看到以下值:

    n [int]: 5
    n1 [int]: 3
    n2 [int]: 5
    sum [int]: 5
    

    喂! 看起来我们做到了! 很好,你已保挽救了 Fibonacci, Inc. 的一天!

  12. 选择“继续”,只是为了确保程序返回正确的值。

    5
    The program '[105260] DotNetDebugging.dll' has exited with code 0 (0x0).
    

    这样会返回正确的输出。

你成功了! 你使用 Visual Studio Code 中的 .NET 调试器调试了你并未编写的某些代码。

在下一单元中,你将了解如何使用内置于 .NET 中的日志记录和跟踪功能,使编写的代码更易于调试。