修复代码中的 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 桌面开发工作负载,然后选择修改。
按照以下步骤创建应用程序:
打开 Visual Studio。 在“开始”窗口中,选择“创建新项目”。
在搜索框中,输入 控制台 ,然后输入 .NET 的 控制台应用 选项之一。
选择“下一步”。
输入项目名称(如 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 异常(运行时错误)。 也就是说,应用在尝试序列化数据时会遇到问题。 由于你以调试模式(已附加调试器)启动应用,调试器的异常帮助程序将你直接转到引发异常的代码,并提供有用的错误消息。
错误消息指示无法将值 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)。 这会在更少的步骤中重启应用。 控制台窗口中会显示以下输出。
你可以看到此输出中的内容不正确。 第三条记录 的名称 和 姓氏 值为空!
这是讨论一种有用的编码做法(通常未充分利用)的好时机,即在函数中使用 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。
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。