如何调试---写给一0基础的同学

了解如何调试是每个应用程序开发生命周期的一个关键方面。通过调试,开发人员不仅可以识别出发生了异常,还可以系统地遍历应用程序的执行,直到找到并修复罪魁祸首代码。无论解决方案是否需要修复一个小的错误,甚至需要重写系统中的大量组件,只要有足够的时间和人力,简单的调试操作就可以(最终)解决几乎所有问题。
然而,尽管调试功能强大,但它也有点难以承受。由于在几十个代码编辑器和集成开发环境(ide)中使用了数百种活动编程语言和框架,在开始调试自己的项目时,确切地知道如何开始可能有些令人吃惊。
我们去调试吧!

单步操作

调试过程和正常应用程序执行之间的第一个重要区别是,调试允许作为开发人员的您进行某种形式的附加交互。当您通常执行应用程式时,它会根据程式码库中提供的逻辑和指令,自行执行所有程式码,通常不需要使用者互动。

另一方面,调试的一个重要组成部分是一个人在应用程序执行时与它交互的能力。这个运行时交互打开了许多通常不可用的有用特性和功能。

调试提供的这些特性中最关键的是控制执行流的能力。与普通应用程序一样,您可以向应用程序发出一些基本命令来启动和停止执行,但是调试允许通过附加命令对执行进行更多的控制。

为了说明这一点,我们将使用一个非常简单的C#代码片段:
DateTime birthday = new DateTime(2000, 1, 1);
int daysOld = (DateTime.Today - birthday).Days;
Logging.Log($"Age is {daysOld} days old.");

与大多数应用程序一样,这些代码将按从上到下的顺序逐行执行。因此,我们首先创建一个生日日期时间,然后得到两个日期之间的天数差。今天和生日,然后最终将结果输出为之前的天数:

Age is 6332 days old.
对于正常的执行,这个过程发生得非常快,当我们向应用程序发出开始命令,告诉它开始执行时,这个过程就开始了。然而,调试引入了一些额外的命令,如果需要,这些命令允许我们停止执行并逐行执行。调试执行流命令的大致列表如下:
  • Start
  • Pause
  • Continue
  • Step Into
  • Step Over
  • Step Out
  • Stop

Start和Stop都是不言而喻的。然而,一旦我们获得了在执行中途Pause的能力,事情就开始变得有趣了。考虑一下我们上面简单的三行应用程序。现在,一旦我们开始执行,我们就会点击暂停。当发生这种情况时,后台运行的调试器进程会在发出pause命令时的代码行停止当前的执行。也许我们在应用程序的第二行停止了执行: int daysOld = (DateTime.Today - birthday).Days;。这意味着我们的应用程序已经执行了第一行,所以我们有一个名为birthday的DateTime值,但是第二行和第三行还没有执行(这意味着我们的daysOld变量是0,int的默认值)。

Step Into

现在我们可以开始使用调试器的step功能了。

大多数应用程序由许多行代码组成,这些代码通常调用其他函数或方法。一个给定的方法可能有很多行代码要在里面执行,它甚至可以在它自己内部调用其他方法或函数。这个过程可以持续不断,越来越深入,通过许多层次的执行。Step Into命令允许我们继续逐行执行,同时遍历所有这些子函数和子方法调用。在我们的例子中,如果我们在应用程序的第2行并使用Step Into,我们只需继续到第3行。但是,在第3行发Logging.Log,它是简单帮助器类的一部分,用于在调试期间输出信息:

public static void Log(string value)
{
#if DEBUG
    Debug.WriteLine(value);
#else
    Console.WriteLine(value);
#endif
}

在进入命令的几个步骤之后,我们将遍历Logging.Log,然后在应用程序关闭之前跳回调用main方法。

Step Over

虽然Step-Into是Step命令中最健壮的一个成员,它通过深入研究代码所做的每个过程调用并跟随它进入兔子洞,但是Step-Over命令在到达它们时不会遍历到方法或函数调用中。相反,顾名思义,Step-Over命令只是步进当前过程中的下一个语句。因此,当我们到达上面的简单应用程序的第3行时,会调用Logging.Log。日志方法时,Step Over命令将通知应用程序继续执行(因此将运行Logging.Log,但它将在后台执行此操作,而不通过调试器单步执行该方法。

Step Out

最后,Step Out将这一趋势向前推进了一步(没有双关语的意思)。当发出Step Out命令时,将执行包含当前执行点的过程的所有剩余行,然后调试器跳出到下一条语句,即首先将执行置于内部过程内部的过程调用之后。换句话说,Step Out跳出当前(本地)方法或函数之外,自动执行本地范围代码的其余部分,而不会停止。

例如,如果我们在Logging.Log,我们在Debug.WriteLine(value)行,当我们发出Step Out命令时,应用程序将立即执行该调用过程中剩余的所有行(Logging.Log),然后它将暂停执行并在应用程序结束时等待,在第三行包含我们的:Logging.Log($"Age is {daysOld} days old.");.

用断点将其停下

单步都很好,但是当应用程序变得比几行代码大得多时,通过pause命令在适当的位置停止执行几乎是不可能的。这就是断点派上用场的地方。断点是放置在特定代码行的有意标记,它指示调试器在到达该行代码时暂时挂起执行-换句话说,在该行中断。在大多数代码编辑器中,只需单击一行代码旁边的左侧空白处,就可以放置断点。这通常会创建一个点或其他标记,指示已创建断点。现在,在调试模式下执行应用程序时,执行将在第一行代码处停止,代码上有一个相关的断点。从那里,可以使用普通的step命令遍历需要检查的代码的相关部分,而不是整个应用程序。

条件断点

尽管断点很有用,但在许多情况下,我们可能希望仅在某些情况下在特定断点停止执行。例如,如果我们有一段代码在数千个对象中循环,而我们只需要为该循环的一个特定迭代调试该代码,那么坐在那里并通过循环代码数千次来达到我们所关心的单循环迭代可能是一个艰苦的过程。
这就是为什么大多数调试器允许我们创建断点条件。一个条件通常只是一个或多个布尔(true/false)语句,通常写在相关应用程序的代码中,用于确定特定断点何时应该触发并停止执行。
例如,这里有两行代码使用PizzaBuilder类创建一个美味的小披萨,然后将生成的实例输出到logger:

// Set Medium size, add Sauce, add Provolone cheese, add Pepperoni, add Olives, then build.
var pizza = new PizzaBuilder(Size.Medium)
                        .AddSauce()
                        .AddCheese(Cheese.Provolone)
                        .AddPepperoni()
                        .AddOlives()
                        .Build();
Logging.Log(pizza);

我们正在创建pizzas,我们在Logging.Log(pizza)行,但我们只希望在我们的pizza上有provolone cheese时触发该断点(并暂停执行)。因此,我们的断点条件将包含以下简单语句:pizza.Cheese == Cheese.Provolone

现在,当执行过程中到达这个断点时,我们的调试器将执行上面的语句,并确认我们的pizzas确实包含provolone cheese。由于此条件为真,我们的断点将触发,调试器将在Logging.Log(pizza)等待我们的进一步命令。

观察局部变量

逐行逐行遍历代码,或者根据需要跳过代码是一个很好的特性,但是如果我们不能检查和评估每一行执行的代码是如何改变对象的值和状态的,那么它就没有多大用处了。这就是进入调试过程的地方。
对于大多数调试器来说,局部变量只是局部变量(在局部范围内定义的变量,例如当前正在执行的方法或函数)的表示。因此,局部变量的集合是当前执行范围内所有局部变量的完整、自动填充的列表,由挂起的代码行决定。
例如,在我们的三行age输出方法末尾暂停执行将显示以下局部变量列表:

NameValueType
args {string[0]} string[]
birthday {1/1/2000 12:00:00 AM} System.DateTime
daysOld 6332 int

 

通过上面的特性,我们就可以完成基本的调试了。虽然调试实践通常包括更多的附加特性,但希望本文的简要介绍是您深入研究自己项目调试的良好起点。

posted on 2020-07-22 08:47  活着的虫子  阅读(318)  评论(0编辑  收藏  举报

导航