调试技术和工具,帮助你编写更好的代码

修复代码中的 bug 和错误可能是一项耗时且有时令人沮丧的任务。 要学会有效地调试需要时间。 Visual Studio 等功能强大的 IDE 可以让你的工作更加轻松。 IDE 可帮助你更快地修复错误并调试代码,并帮助你编写更好的代码,并减少 bug。 本文提供“bug 修复”过程的整体视图,以便了解何时使用代码分析器、何时使用调试器、如何修复异常以及如何编写意向代码。 如果已知道需要使用调试器,请参阅 “首先查看调试器”。

本文介绍如何使用 IDE 提高编码会话的效率。 我们讨论了多个任务,例如:

  • 使用 IDE 的代码分析器准备代码以供调试

  • 如何修复异常(运行时错误)

  • 如何有目的地编码来最大程度减少 bug(使用断言)

  • 何时使用调试器

为了演示这些任务,我们显示了尝试调试应用时可能会遇到的一些最常见的错误和 bug 类型。 尽管示例代码为 C#,但概念信息通常适用于 Visual Studio 支持的 C++、Visual Basic、JavaScript 和其他语言(除非另有说明)。 屏幕截图位于 C# 中。

创建一个示例应用,其中包含一些 bug 和错误

以下代码包含一些可以使用 Visual Studio IDE 修复的 bug。 此应用程序是一个简单的工具,用于模拟从某些操作获取 JSON 数据,将数据反序列化为对象,并用新数据更新简单列表。

若要创建应用,必须安装 Visual Studio 并安装 .NET 桌面开发 工作负载。

  • 如果尚未安装 Visual Studio,请转到 Visual Studio 下载 页免费安装。

  • 如果需要安装工作负载但已有 Visual Studio,请选择 “工具>获取工具和功能”。 Visual Studio 安装程序将启动。 选择 .NET 桌面开发工作负载,然后选择修改。

按照以下步骤创建应用程序:

  1. 打开 Visual Studio。 在“开始”窗口中,选择“创建新项目”。

  2. 在搜索框中,输入 控制台 ,然后输入 .NET 的 控制台应用 选项之一。

  3. 选择“下一步”。

  4. 输入项目名称(如 Console_Parse_JSON),然后选择“ 下一步 ”或“ 创建”(如适用)。

    选择建议的目标框架或 .NET 10,然后选择“ 创建”。

    如果未看到适用于 .NET 项目的 控制台应用 模板,请转到 “工具>获取工具和功能”,这将打开 Visual Studio 安装程序。 选择 .NET 桌面开发工作负载,然后选择修改。

    Visual Studio 将创建控制台项目,该项目显示在右窗格中的解决方案资源管理器中。

项目准备就绪后,将项目 Program.cs 文件中的默认代码替换为以下示例代码:

using System;
using System.Collections.Generic;
using System.Runtime.Serialization.Json;
using System.Runtime.Serialization;
using System.IO;

namespace Console_Parse_JSON
{
    class Program
    {
        static void Main(string[] args)
        {
            var localDB = LoadRecords();
            string data = GetJsonData();

            User[] users = ReadToObject(data);

            UpdateRecords(localDB, users);

            for (int i = 0; i < users.Length; i++)
            {
                List<User> result = localDB.FindAll(delegate (User u) {
                    return u.lastname == users[i].lastname;
                    });
                foreach (var item in result)
                {
                    Console.WriteLine($"Matching Record, got name={item.firstname}, lastname={item.lastname}, age={item.totalpoints}");
                }
            }

            Console.ReadKey();
        }

        // Deserialize a JSON stream to a User object.
        public static User[] ReadToObject(string json)
        {
            User deserializedUser = new User();
            User[] users = { };
            MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(json));
            DataContractJsonSerializer ser = new DataContractJsonSerializer(users.GetType());

            users = ser.ReadObject(ms) as User[];

            ms.Close();
            return users;
        }

        // Simulated operation that returns JSON data.
        public static string GetJsonData()
        {
            string str = "[{ \"points\":4o,\"firstname\":\"Fred\",\"lastname\":\"Smith\"},{\"lastName\":\"Jackson\"}]";
            return str;
        }

        public static List<User> LoadRecords()
        {
            var db = new List<User> { };
            User user1 = new User();
            user1.firstname = "Joe";
            user1.lastname = "Smith";
            user1.totalpoints = 41;

            db.Add(user1);

            User user2 = new User();
            user2.firstname = "Pete";
            user2.lastname = "Peterson";
            user2.totalpoints = 30;

            db.Add(user2);

            return db;
        }
        public static void UpdateRecords(List<User> db, User[] users)
        {
            bool existingUser = false;

            for (int i = 0; i < users.Length; i++)
            {
                foreach (var item in db)
                {
                    if (item.lastname == users[i].lastname && item.firstname == users[i].firstname)
                    {
                        existingUser = true;
                        item.totalpoints += users[i].points;

                    }
                }
                if (existingUser == false)
                {
                    User user = new User();
                    user.firstname = users[i].firstname;
                    user.lastname = users[i].lastname;
                    user.totalpoints = users[i].points;

                    db.Add(user);
                }
            }
        }
    }

    [DataContract]
    internal class User
    {
        [DataMember]
        internal string firstname;

        [DataMember]
        internal string lastname;

        [DataMember]
        // internal double points;
        internal string points;

        [DataMember]
        internal int totalpoints;
    }
}

接下来,检查是否已在文件的开头自动添加using System.Text。 默认情况下,最新版本的 Visual Studio 会自动启用“粘贴时添加缺少的 using 指令”选项。 如果 using System.Text 已添加,请注释掉此代码行。

// using System.Text;

找到红色和绿色波浪线!

在尝试启动示例应用并运行调试器之前,请检查代码编辑器中的代码是否有红色和绿色波浪线。 这些表示由 IDE 代码分析器标识的错误和警告。 红色波浪线是编译时错误,必须先修复这些错误,然后才能运行代码。 绿色波浪线是警告。 尽管你经常可以在不修复警告的情况下运行应用,但它们可能是 bug 的来源,你经常通过调查它们来节省时间和麻烦。 如果更喜欢列表视图,这些警告和错误也会显示在 “错误列表 ”窗口中。

在示例应用中,可以看到几条红色波浪线,表示需要修复问题,还有一条绿色波浪线,表示需要调查。 下面是第一个错误。

屏幕截图显示 Visual Studio 中作为红色波浪线的错误。

将错误显示为红色波浪线的屏幕截图。

若要修复此错误,可以查看由灯泡图标表示的 IDE 的另一个功能。

检查灯泡!

第一个红色波浪线表示编译时错误。 将鼠标悬停在它上,你会看到消息 The name `Encoding` does not exist in the current context。

请注意,此错误显示左下角的灯泡图标。 除了螺丝刀图标 螺丝刀图标外,灯泡图标 灯泡图标 还表示快速操作,可以帮助你内联修复或重构代码。 灯泡表示 应 解决的问题。 螺丝刀适用于你可能选择修复的问题。 单击左侧的“使用 System.Text”,使用第一个建议的修复来解决此错误。

展示如何在 Visual Studio 中使用灯泡修复代码的截图。

显示如何使用灯泡修复代码的屏幕截图。

选择此项时,Visual Studio 会将 using System.Text 语句添加到 Program.cs 文件的顶部,红色波浪线消失。 (如果不确定建议的修补程序应用的更改,请在应用修补程序之前选择右侧的 “预览更改 ”链接。

前面的错误是通常通过向代码添加新 using 语句来修复的常见错误。 有几种常见的类似错误,例如 The type or namespace "Name" cannot be found. 此类错误可能指示缺少程序集引用(右键单击项目,选择 “添加>引用”)、拼写错误的名称或需要添加的缺失库(对于 C#,右键单击项目并选择 “管理 NuGet 包”)。

修复剩余错误

修复剩余的错误和警告

在此代码中,还有几条需要检查的波浪线。 在这里,你会看到一个常见的类型转换错误。 将鼠标悬停在波浪线上时,可以看到代码尝试将字符串转换为整数,但不支持这样做,除非添加显式代码来进行转换。

显示 Visual Studio 中类型转换错误的屏幕截图。

显示类型转换错误的屏幕截图。

由于代码分析器无法猜出你的意图,所以这次没有灯泡可以帮助你。 若要修复此错误,需要知道代码的意图。 在此示例中,不难看出points应该是一个数值(整数),因为您正尝试将points加到totalpoints中。

若要修复此错误,请将 points 类的 User 成员从:

[DataMember]
internal string points;

到此:

[DataMember]
internal int points;

代码编辑器中的红色波浪线消失。

接下来,将鼠标悬停在 points 数据成员的声明中的绿色波浪线上。 代码分析器会告诉你变量从未分配过值。

未分配变量的警告消息

通常,这表示需要修复的问题。 但是,在示例应用中,实际上是在反序列化过程中将数据 points 存储在变量中,然后将该值添加到 totalpoints 数据成员。 在此示例中,你知道代码的意图,并且可以安全地忽略警告。 但是,如果要消除警告,可以替换以下代码:

item.totalpoints = users[i].points;

替换为以下内容:

item.points = users[i].points;
item.totalpoints += users[i].points;

绿色波浪线消失了。

修复异常

当你修复了所有红色波浪线,且至少查看过所有绿色波浪线后,就可以启动调试器并运行该应用了。

如果已修复所有红色波浪线表示的问题,并解决(或者至少核查了)所有绿色波浪线表示的问题,就可以启动调试器并运行应用了。

按 F5 (调试>开始调试)或在调试工具栏中点击开始调试按钮开始调试。

此时,示例应用会引发 SerializationException 异常(运行时错误)。 也就是说,应用在尝试序列化数据时会遇到问题。 由于你以调试模式(已附加调试器)启动应用,调试器的异常帮助程序将你直接转到引发异常的代码,并提供有用的错误消息。

发生 SerializationException

错误消息指示无法将值 4o 分析为整数。 因此,在此示例中,你知道数据是错误的: 4o 应该是 40。 但是,如果你不在实际场景中控制数据(假设你从 Web 服务获取数据),你对此有何作? 如何解决此问题?

遇到异常时,需要提问(并回答)几个问题:

  • 这个异常是否仅仅是你可以修复的错误? 或者,

  • 你的用户可能会遇到此异常吗?

如果是前者,请修复错误。 (在示例应用中,需要修复错误的数据。如果是后者,可能需要使用 try/catch 块处理代码中的异常(我们将在下一部分中查看其他可能的策略)。 在示例应用中,替换以下代码:

users = ser.ReadObject(ms) as User[];

使用此代码:

try
{
    users = ser.ReadObject(ms) as User[];
}
catch (SerializationException)
{
    Console.WriteLine("Give user some info or instructions, if necessary");
    // Take appropriate action for your app
}

try/catch 块会降低性能,因此尽量只在必要情况下才使用它们,即下面两种情况:(a) 它们可能出现在应用的发行版中;(b) 方法文档指示你检查该异常(假定文档已经完善!)。 在许多情况下,可以适当地处理异常,用户永远不需要知道它。

下面是异常处理的重要提示:

  • 避免使用空的 catch 块,例如 catch (Exception) {},因为它不会采取适当的行动来暴露或处理错误。 空的或缺乏信息的 Catch 块可能会隐藏异常,从而使代码更难调试,而不是更容易。

  • 在引发异常的特定函数周围使用 try/catch 块(在示例应用中为 ReadObject)。 如果在较大的代码块周围使用它,最终会隐藏错误的位置。 例如,不要在对父函数 try/catch 的调用周围使用 ReadToObject 块(如此处所示),否则你将不知道异常发生的确切位置。

    // Don't do this
    try
    {
        User[] users = ReadToObject(data);
    }
    catch (SerializationException)
    {
    }
    
  • 对于应用中包括的不熟悉函数(尤其是与外部数据(如 Web 请求)交互的函数,请检查文档以查看函数可能引发的异常。 这可以是正确错误处理和调试应用的关键信息。

对于示例应用程序,通过将SerializationException更改为GetJsonData来修复4o方法中的40问题。

小窍门

如果有 Copilot,则可以在调试异常时获得 AI 帮助。 只需查找“使用 Copilot 分析”或“询问 Copilot”按钮。询问 Copilot 按钮的屏幕截图。 有关详细信息,请参阅使用 Copilot 进行调试。

使用断言阐明代码意图

在调试工具栏中选择“重启重启应用”按钮(Ctrl + Shift + F5)。 这会在更少的步骤中重启应用。 控制台窗口中会显示以下输出。

输出中的 Null 值

你可以看到此输出中的内容不正确。 第三条记录 的名称 和 姓氏 值为空!

这是讨论一种有用的编码做法(通常未充分利用)的好时机,即在函数中使用 assert 语句。 通过添加以下代码,可以包含运行时检查,以确保firstname和lastname都不是null。 将以下代码替换到UpdateRecords方法中:

if (existingUser == false)
{
    User user = new User();
    user.firstname = users[i].firstname;
    user.lastname = users[i].lastname;

替换为以下内容:

// Also, add a using statement for System.Diagnostics at the start of the file.
Debug.Assert(users[i].firstname != null);
Debug.Assert(users[i].lastname != null);
if (existingUser == false)
{
    User user = new User();
    user.firstname = users[i].firstname;
    user.lastname = users[i].lastname;

通过在开发过程中将此类语句添加到 assert 函数中,可以帮助指定代码的意图。 在前面的示例中,我们指定以下项:

  • 需要有效的字符串来表示名字
  • 需要有效的字符串来表示姓氏

通过以这种方式指定意向,可以强制实施要求。 这是一种简单便捷的方法,可用于在开发期间显示 bug。 (assert 语句也用作单元测试中的主元素。

在调试工具栏中选择“重启重启应用”按钮(Ctrl + Shift + F5)。

注释

代码 assert 仅在调试版本中处于活动状态。

重新启动时,调试器会在 assert 语句上暂停,因为表达式 users[i].firstname != null 的计算结果为 false 而不是 true。

断言解析为 false

assert 错误表示存在需要调查的问题。 assert 可以涵盖许多不一定会出现异常的情况。 在此示例中,用户看不到异常,并且< c0 />值会以< c1 />的形式添加到您的记录列表中。 此条件可能会导致以后出现问题(如在控制台输出中看到),并且可能更难调试。

注释

对 null 值调用方法时,将生成 NullReferenceException。 通常情况下,应避免用 try/catch 块调试一般异常,即与特定库函数无关的异常。 任何对象都可以引发 NullReferenceException. 如果不确定,请查看库函数的文档。

在调试过程中,最好保留特定 assert 语句,直到你知道需要将其替换为实际代码修复。 假设你确定用户可能会在应用的发布版本中遇到异常。 在这种情况下,必须重构代码,以确保应用不会引发致命异常或导致其他错误。 因此,若要修复此代码,请替换以下代码:

if (existingUser == false)
{
    User user = new User();

使用此代码:

if (existingUser == false && users[i].firstname != null && users[i].lastname != null)
{
    User user = new User();

使用此代码,您可以满足代码要求,并确保不会向数据中添加包含firstname值为lastname或null的记录。

在此示例中,我们在循环中添加了两 assert 个语句。 通常,使用 assert时,最好在函数或方法的入口点(开头)添加 assert 语句。 你当前正在查看 UpdateRecords 示例应用中的方法。 在此方法中,如果任一方法参数是 null,那么你会遇到问题,因此请在函数入口处使用 assert 语句来检查它们。

public static void UpdateRecords(List<User> db, User[] users)
{
    Debug.Assert(db != null);
    Debug.Assert(users != null);

对于前面的语句,你的意图是在更新任何内容之前加载现有数据(db)并检索新数据(users)。

可以使用任何解析为assert或true的表达式与false一起使用。 因此,例如,可以添加如下所示的 assert 语句。

Debug.Assert(users[0].points > 0);

如果要指定以下意向,则上述代码非常有用:更新用户记录需要大于零(0)的新点值。

在调试器中检查代码

好吧,现在你已修复了示例应用出错的所有关键内容,你可以转到其他重要内容!

我们向您展示了调试器的异常帮助程序,但调试器是一个更强大的工具,还可以执行其他操作,例如单步调试代码并检查其变量。 这些更强大的功能在许多方案中非常有用,尤其是以下方案:

  • 你正在尝试隔离代码中的运行时 bug,但无法使用前面讨论过的方法和工具来执行此操作。

  • 你想要验证代码,也就是说,在代码运行时监视它,以确保其行为如预期,并完成所需的任务。

    在代码运行时对其进行监视具有指导意义。 通过这种方式,你可以更深入地了解代码,并且常可在出现明显征兆之前识别 bug。

若要了解如何使用调试器的基本功能,请参阅 针对绝对初学者的调试。

修复性能问题

另一种类型的 Bug 包括低效代码,导致应用运行缓慢或使用过多内存。 通常,性能优化是在应用开发的后期进行的。 但是,可以提前遇到性能问题(例如,你会看到应用的某些部分运行缓慢),可能需要提前使用分析工具测试应用。 有关分析工具(如 CPU 使用情况工具和内存分析器)的详细信息,请参阅 首先查看分析工具。

本文介绍了如何避免和修复代码中的许多常见 bug 以及何时使用调试器。 接下来,详细了解如何使用 Visual Studio 调试器修复 bug。