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

修复代码中的 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 8,然后选择“创建”。

    如果未看到适用于 .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;
    }
}

找到红色和绿色波浪线!

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

在示例应用中,你会看到几个需要修复的红色波浪线,以及一个需要调查的绿色波浪线。 下面是第一个错误。

显示为红色波浪线的错误

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

检查灯泡!

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

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

使用灯泡修复代码

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

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

修复剩余的错误和警告

在此代码中,还有几个需要查看的波浪线。 在这里,你会看到一个常见的类型转换错误。 当将鼠标悬停在波浪线时,你会看到代码尝试将字符串转换为 int。除非添加显式转换代码,否则这个转换不被支持。

类型转换错误

由于代码分析器无法猜出你的意图,所以这次没有灯泡可以帮助你。 若要修复此错误,需要知道代码的意图。 在此示例中,不难看出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”按钮 Screenshot of Ask Copilot button.。 有关详细信息,请参阅使用 Copilot 进行调试

使用断言阐明代码意图

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

输出中的 Null 值

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

这是讨论一种有用的编码做法(通常未充分利用)的好时机,即在函数中使用 assert 语句。 通过添加以下代码,可以包含运行时检查,以确保firstnamelastname都不是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值为lastnamenull的记录。

在此示例中,我们在循环中添加了两 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)。

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

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

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

在调试器中检查代码

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

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

  • 你尝试在代码中隔离运行时 bug,但无法使用前面讨论的方法和工具做到这一点。

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

    在代码运行时观察它是很有启发性的。 可以这样更深入地了解您的代码,并经常在它们表现出任何明显症状之前识别 bug。

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

修复性能问题

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

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