零基础调试
通常情况下,作为软件开发人员,我们编写的代码并不总是能够实现我们期望的效果。 有时它做一些完全不同的事情! 当意外发生时,下一个任务是找出原因,尽管我们可能很想只盯着代码看几个小时,但使用调试工具或调试器更容易、更高效。
遗憾的是,调试器无法神奇地揭示代码中的所有问题或“bug”。 调试 意味着在调试工具(如 Visual Studio)中逐步运行代码,以找到你犯了编程错误的确切点。 然后,你了解在代码和调试工具中需要执行哪些更正,通常允许你进行临时更改,以便可以继续运行程序。
有效地使用调试器也是一项技能,需要时间和实践才能学习,但最终是每个软件开发人员的基本任务。 在本文中,我们将介绍调试的核心原则,并提供入门提示。
通过问自己正确的问题来澄清问题
它有助于澄清在尝试修复之前遇到的问题。 我们预计你已在代码中遇到问题,否则你不会在这里尝试弄清楚如何调试它! 因此,在开始调试之前,请确保已确定要解决的问题:
你期望代码做什么?
相反,发生了什么情况?
如果在运行应用时遇到错误(异常),那可能是件好事! 异常是运行代码时遇到的意外事件,通常是某种类型的错误。 调试工具可将你带到发生异常的代码中的确切位置,并有助于调查可能的修复。
如果发生其他情况,问题的症状是什么? 您是否已经怀疑问题在您的代码中发生在哪里? 例如,如果代码显示了一些文本,但文本不正确,那么你知道要么是你的数据有问题,要么是设置显示文本的代码有某种错误。 通过在调试器中逐步执行代码,可以检查变量的每一个变化,从而确切地发现不正确的值是何时以及如何被分配的。
检查假设
在调查 bug 或错误之前,请想一想促使您预期特定结果的那些假设。 即使你正视调试器中问题的原因,隐藏或未知的假设也能妨碍识别问题。 你可能有一长串可能的假设! 下面是一些问题,要求自己挑战你的假设。
是否使用正确的 API(即正确的对象、函数、方法或属性)? 你使用的 API 可能无法执行你认为它的作用。 (在调试器中检查 API 调用后,修复它可能需要访问文档以帮助识别正确的 API。
是否正确使用 API? 也许你使用了正确的 API,但没有以正确的方式使用它。
代码是否包含任何拼写错误? 某些拼写错误(如变量名称的简单拼写错误)可能很难看到,尤其是在使用不需要声明变量的语言时。
你是否对代码进行了更改,并假设它与你看到的问题无关?
你是否期望对象或变量包含与实际发生的情况不同的特定值(或某种类型的值) ?
你知道代码的意图吗? 通常更难调试其他人的代码。 如果这不是你的代码,你可能需要花时间仔细了解代码的具体功能,然后才能有效地进行调试。
提示
编写代码时,请从小型代码开始,以正常运行的代码开始! (此处提供了很好的示例代码。)有时,从一小段代码开始,可以更轻松地修复一组大型或复杂的代码,这些代码演示了你尝试实现的核心任务。 然后,可以增量修改或添加代码,并在每个点测试错误。
通过质疑假设,可以减少在代码中查找问题所需的时间。 还可以减少解决问题所需的时间。
在调试模式下逐步完成代码,以查找问题发生位置
正常运行应用时,只有在代码运行后,才会看到错误和不正确的结果。 程序也可能意外终止,而不会告诉你原因。
在调试器中运行应用(也称为 调试模式)时,调试器会主动监视程序运行时发生的所有内容。 此外允许在任何时候暂停应用以检查其状态,然后逐行单步调试代码以查看发生的每个细节。
在 Visual Studio 中,您可以使用 F5 键(或通过 调试>启动调试 菜单命令、启动调试 按钮,以及调试工具栏中的 )进入调试模式。 如果发生任何异常,Visual Studio 的异常帮助程序会将你转到发生异常的确切点,并提供其他有用的信息。 有关如何在代码中处理异常的详细信息,请参阅 调试技术和工具。
如果没有出现异常,您可能已经知道在代码中哪里可以找到问题。 可在此步骤中结合使用断点和调试器,这样便有机会更仔细地检查代码。 断点是可靠调试的最基本和最重要的功能。 断点指示 Visual Studio 应暂停正在运行的代码的位置,以便你可以查看变量的值或内存的行为,即代码运行的顺序。
在 Visual Studio 中,可以通过单击代码行旁边的左边距快速设置断点。 或者将光标放在一行上,然后按下 F9。
为了帮助说明这些概念,我们将引导你完成一些已有多个 bug 的示例代码。 我们使用的是 C#,但调试功能适用于 Visual Basic、C++、JavaScript、Python 和其他受支持的语言。 还提供了 Visual Basic 的示例代码,但屏幕截图位于 C# 中。
创建一个带有一些错误的示例应用
接下来,创建一个存在一些 bug 的应用程序。
必须安装 Visual Studio,并安装 .NET 桌面开发 工作负载。
如果尚未安装 Visual Studio,请转到 Visual Studio 下载 页免费安装。
如果需要安装工作负载但已安装 Visual Studio,请选择“工具”>“获取工具和功能”。 Visual Studio 安装程序将启动。 选择 .NET 桌面开发 工作负载,然后选择 修改。
打开 Visual Studio。
在开始窗口中,选择 创建新项目。 在搜索框中键入 控制台,选择 C# 或 Visual Basic 作为语言,然后选择适用于 .NET 的控制台应用。 选择“下一步”。 键入 ConsoleApp_FirstApp 作为项目名称,然后选择“下一步”。
如果使用其他项目名称,则需要在复制示例代码时修改命名空间值以匹配项目名称。
选择建议的目标框架或 .NET 8,然后选择 创建。
如果未看到适用于 .NET 的 控制台应用 项目模板,请转到 工具>获取工具和功能,这将打开 Visual Studio 安装程序。 选择 .NET 桌面开发 工作负载,然后选择 修改。
Visual Studio 将创建控制台项目,该项目显示在 解决方案资源管理器 右窗格中。
在 Program.cs(或 Program.vb)中,将所有默认代码替换为以下代码。 (首先选择正确的语言选项卡 C# 或 Visual Basic。
using System; using System.Collections.Generic; namespace ConsoleApp_FirstApp { class Program { static void Main(string[] args) { Console.WriteLine("Welcome to Galaxy News!"); IterateThroughList(); Console.ReadKey(); } private static void IterateThroughList() { var theGalaxies = new List<Galaxy> { new Galaxy() { Name="Tadpole", MegaLightYears=400, GalaxyType=new GType('S')}, new Galaxy() { Name="Pinwheel", MegaLightYears=25, GalaxyType=new GType('S')}, new Galaxy() { Name="Cartwheel", MegaLightYears=500, GalaxyType=new GType('L')}, new Galaxy() { Name="Small Magellanic Cloud", MegaLightYears=.2, GalaxyType=new GType('I')}, new Galaxy() { Name="Andromeda", MegaLightYears=3, GalaxyType=new GType('S')}, new Galaxy() { Name="Maffei 1", MegaLightYears=11, GalaxyType=new GType('E')} }; foreach (Galaxy theGalaxy in theGalaxies) { Console.WriteLine(theGalaxy.Name + " " + theGalaxy.MegaLightYears + ", " + theGalaxy.GalaxyType); } // Expected Output: // Tadpole 400, Spiral // Pinwheel 25, Spiral // Cartwheel, 500, Lenticular // Small Magellanic Cloud .2, Irregular // Andromeda 3, Spiral // Maffei 1, 11, Elliptical } } public class Galaxy { public string Name { get; set; } public double MegaLightYears { get; set; } public object GalaxyType { get; set; } } public class GType { public GType(char type) { switch(type) { case 'S': MyGType = Type.Spiral; break; case 'E': MyGType = Type.Elliptical; break; case 'l': MyGType = Type.Irregular; break; case 'L': MyGType = Type.Lenticular; break; default: break; } } public object MyGType { get; set; } private enum Type { Spiral, Elliptical, Irregular, Lenticular} } }
此代码的目的是显示银河系名称、银河系距离以及银河系在列表中的所有类型。 若要调试,请务必了解代码的意图。 要在输出中显示的列表中一行格式如下:
星系名称,距离,星系类型。
运行应用
按 F5 或 启动调试 按钮 位于代码编辑器上方的调试工具栏中。
应用启动,调试器不会向我们显示异常。 但是,在控制台窗口中看到的输出不是你所期望的。 下面是预期输出:
Tadpole 400, Spiral
Pinwheel 25, Spiral
Cartwheel, 500, Lenticular
Small Magellanic Cloud .2, Irregular
Andromeda 3, Spiral
Maffei 1, Elliptical
但是,你会看到以下输出:
Tadpole 400, ConsoleApp_FirstApp.GType
Pinwheel 25, ConsoleApp_FirstApp.GType
Cartwheel, 500, ConsoleApp_FirstApp.GType
Small Magellanic Cloud .2, ConsoleApp_FirstApp.GType
Andromeda 3, ConsoleApp_FirstApp.GType
Maffei 1, 11, ConsoleApp_FirstApp.GType
查看输出和代码,我们知道 GType
是存储银河系类型的类的名称。 我们试图显示实际的星系类型(如“螺旋”),而不是类名!
调试应用
在应用仍在运行的情况下,插入断点。
在
foreach
循环中,在Console.WriteLine
方法旁右键单击以打开上下文菜单,并从浮出菜单中选择“断点”>“插入断点”。foreach (Galaxy theGalaxy in theGalaxies) { Console.WriteLine(theGalaxy.Name + " " + theGalaxy.MegaLightYears + ", " + theGalaxy.GalaxyType); }
设置断点时,左边距中会显示一个红点。
当你发现输出中有问题时,你通过查看在调试器中设置输出的之前的代码开始调试。
选择调试工具栏中的“重启
”按钮(Ctrl + Shift + F5)。
应用在设置的断点处暂停。 黄色突出显示指示调试器暂停的位置(尚未执行黄色代码行)。
将鼠标悬停在右侧
GalaxyType
变量上,然后在扳手图标左侧展开theGalaxy.GalaxyType
。 可以看到,GalaxyType
包含属性MyGType
,并且属性值设置为Spiral
。“螺旋”正是你期望输出到控制台的正确值! 因此,在运行应用时,你可以访问此代码中的值,这是一个很好的开端。 在此方案中,我们使用不正确的 API。 让我们看看是否可以在调试器中运行代码时解决此问题。
在相同的代码中进行调试时,将光标放在
theGalaxy.GalaxyType
的末尾并将其更改为theGalaxy.GalaxyType.MyGType
。 虽然可以进行编辑,但代码编辑器会显示错误(红色波浪线)。 (在 Visual Basic 中,错误未显示,并且此代码部分有效。按 F11(调试>单步执行 或调试工具栏中的 单步执行 按钮)执行当前代码行。
F11 一次调试一个语句(并执行代码)。 F10(逐过程执行)是类似的命令,在学习如何使用调试器时两者都非常有用。
当你尝试在调试器中进行调试时,会弹出“热重载”对话框,提示无法编译已编辑的内容。
将显示“编辑并继续”对话框,指示无法编译已编辑的内容。
备注
若要调试 Visual Basic 示例代码,请跳过接下来的几个步骤,直到被指示单击调试工具栏中显示“重启应用”按钮的“重启
按钮。
在“热重载”或“编辑并继续”消息框中选择“编辑”。 现在,在 错误列表 窗口中看到错误消息。 错误指示
'object'
不包含MyGType
的定义。尽管我们用类型
GType
(具有MyGType
属性)设置每个星系,但调试器无法将theGalaxy
对象识别为GType
类型的对象。 这是怎么回事? 想查看设置星系类型的任何代码。 执行此操作时,可以看到GType
类肯定具有MyGType
的属性,但有些内容不正确。 有关object
的错误消息被证明是线索;对语言解释器,该类型似乎是object
类型的对象,而不是GType
类型的对象。查看与设置 galaxy 类型相关的代码,你会发现
Galaxy
类的GalaxyType
属性被指定为object
而不是GType
。public object GalaxyType { get; set; }
更改上述代码,如下所示:
public GType GalaxyType { get; set; }
选择调试工具栏中的“重启”
按钮 (Ctrl + Shift + F5) 以重新编译代码并重启。
现在,当调试器暂停
Console.WriteLine
时,你可以将鼠标悬停在theGalaxy.GalaxyType.MyGType
上,并看到该值已正确设置。单击左侧边距中的断点圆(或右键单击并选择“断点>删除断点”),然后按 F5 继续。
应用运行并显示输出。 看起来不错,但你注意到了一件事。 你预计小麦切拉尼云星系在控制台输出中显示为不规则的星系,但它根本不显示星系类型。
Tadpole 400, Spiral Pinwheel 25, Spiral Cartwheel, 500, Lenticular Small Magellanic Cloud .2, Andromeda 3, Spiral Maffei 1, Elliptical
在
switch
语句之前(在 Visual Basic 中的Select
语句之前)在此代码行上设置断点。public GType(char type)
此代码是设置星系类型的位置,因此我们希望仔细查看它。
选择 重启
按钮(Ctrl + Shift + F5)重启。
调试器会在设置断点的代码行上暂停。
将鼠标悬停在
type
变量上。 随即看到S
的值(字符代码后面)。 你对I
的值感兴趣,因为你知道这是一种不规则的星系类型。按 F5,再将鼠标悬停在
type
变量上。 重复此步骤,直到看到type
变量中的I
值。现在请按 F11(“调试”>“单步执行”)。
按 F11,直到在代码行的
switch
语句中遇到“I”值时停止(Visual Basic 中的Select
语句)。 在这里,你会看到一个由于拼写错误而造成的明显问题。 你希望代码前进到将MyGType
设置为不规则星系类型的位置,但调试器会完全跳过此代码,并在switch
语句的default
节(Visual Basic 中的Else
语句)上暂停。查看代码时,会在
case 'l'
语句中看到拼写错误。 它应为case 'I'
。选择
case 'l'
的代码并将其替换为case 'I'
。删除断点,然后选择“重启”按钮以重启该应用。
这些 bug 已经修复,现在你会看到期待的输出结果!
按任意键完成应用。
总结
看到问题时,请使用调试器和 步骤命令(如 F10 和 F11)查找问题的代码区域。
备注
如果难以确定出现问题的代码区域,请在出现问题之前运行的代码中设置断点,然后使用单步执行命令,直到看到问题清单。 此外可以使用跟踪点来将消息记录到“输出”窗口。 通过查看已记录的消息(并注意哪些消息尚未记录!),通常可以隔离出存在问题的代码区域。 可能需要多次重复此过程才能缩小其范围。
找到出现问题的代码区域时,请使用调试器进行调查。 若要查找问题的原因,请在调试器中运行应用时检查问题代码:
检查变量,并检查它们是否包含它们应包含的值的类型。 如果发现一个错误值,请找出设置错误的值的位置(若要查找设置值的位置,可能需要重启调试器、查看 调用堆栈,或同时查看两者)。
检查应用程序是否正在执行所需的代码。 (例如,在示例应用程序中,我们预期
switch
语句的代码将 galaxy 类型设置为“不规则”,但应用由于拼写错误而跳过了代码。
提示
你使用调试器来帮助你查找错误。 仅在知道代码含义的情况下,调试工具才能查找 bug。 如果开发人员表示该意向,工具只能知道代码的意图。 编写单元测试就是这样做的。
后续步骤
本文介绍了一些常规调试概念。 接下来,可以开始了解有关调试器的详细信息。