第20章 循环
计算机最擅长做的事情就是“重复”——像小孩子一样不厌其烦地重复做一件事,而且重复的速度很快,可以在1毫秒的时间内列出你全部的Facebook好友。
本章将学习如何编写一段重复执行的程序:用App Inventor中提供的两个循环块,实现某些代码块的重复执行,来替代反复复制及粘贴代码块。首先学习与列表有关的循环块,例如,给电话簿列表中的每个号码发送一条短信。然后,通过一个自然数求和的例子,学习使用条件循环块。通过学习,你将了解如何使用循环来有效地简化程序。
控制程序的执行:分支及循环
在前几章中我们学到,应用由一系列的事件处理程序组成,这些程序定义了应用的功能,而它们由一系列的指令构成。在这些指令中,有些按顺序执行,而有些只有在满足某些条件时才能运行,这就是所谓的编程语言中的分支结构。
循环是编程语言中另一种重要的结构。就像用“如果”及“否则,如果”块来构造分支结构一样,循环块用来构造程序中的循环结构,换句话说,循环块中的程序在执行完一系列指令后,重新跳回到这些指令的起点并再次运行,如图20-1所示。应用在运行过程中,内部的程序计数器会跟踪即将执行的下一个指令。到目前为止,在你学过的应用中,程序计数器对事件处理程序的跟踪都是自上而下的(虽然中间可能有条件分支),或者说,程序的执行顺序是自上而下的。但是在循环块中,程序计数器会随着这些重复执行的块一起循环,连续地执行一组相同的操作。
App Inventor中提供了几种类型的循环块,本章将重点讲解遍历列表循环以及条件循环。遍历列表循环是针对列表中的每一项执行某些特定的操作,例如,向电话簿列表中的每个电话号码发送一条短信。
条件循环的应用比遍历列表循环更为普遍。条件循环中的程序块会一直重复运行,直到某个条件不再满足时才退出循环。条件循环可以用于数学公式的计算,如求n个连续自然数的和,或求n的阶乘,等等。此外,条件循环也可以用于同时处理两个列表,而遍历列表循环每次只能处理一个列表。
遍历列表循环
在第18章里,我们示范了一个“随机拨号”应用。这种随机拨打电话的方式虽然有时能拨通,但是如果你有一个像我这样的朋友,这种呼叫不见得总能得到应答。可以采取另一种方式,给所有列表中的朋友发短信说“想念你!”,然后看谁最先回复你(或谁的回复最温馨)。
实现这一功能最简单的方法就是复制代码:先写好发给一个人的代码块,然后复制粘贴并修改接收人的电话号码,如图20-2所示。
如果只有少量的块,用这种暴力的复制粘贴方式也还说得过去,但是假如你面临大量的数据,或者数据经常发生变化,每次变化你都要修改程序,只为添加或删除一个电话号码,我想你是不会情愿做这件事的。
遍历列表块提供了一个更好的解决方案,可以定义一个包含所有电话号码的列表变量,如电话簿,然后用遍历列表块将发送一次短信的代码块包围起来,从而实现群发功能,如图20-3所示。
上述代码可以解读为:针对电话簿列表中的每一项(电话号码),设短信收发器1的电话号码属性为列表中的项,并发送短信。
在遍历列表块中有两个参数:第一个参数是一个列表,是循环操作要遍历的列表;第二个参数是遍历列表块自带的“占位符变量”{![在表达式中占据某个位置的、无实际意义的符号,在表达式被调用时将赋予它具体的含义。——译者注]},默认变量名为“项”,它代表循环操作当前正在处理的列表项。你可以直接采用“项”这个默认的名称,也可以将其修改为有意义的名称,例如,这个例子中改为“电话号码”。
如果一个列表中有三个列表项,那么遍历列表块内部的代码将被执行三次。这些重复执行的代码被称为循环体,当程序计数器到达循环体的末尾时,将重新跳回到循环的起点,并再次执行循环体(注意,循环的起点不等于循环体的起点。——译者注)。
近距离观察循环
理解循环的执行过程,这是我们深入学习编程的基础。因此,我们要对遍历列表块的运行机制进行细致的分析。当用户点击群发按钮时,调用事件处理程序,首先为短信收发器设置要发送的短信内容,将其设置为“想念你!”,这个块只需要执行一次。
然后开始执行遍历列表块。首先解释一下循环的起点:在遍历列表块中的循环体开始执行以前,程序会先将占位符变量“项”设置为电话簿列表的第一项(111-1111)。这一步是自动完成的,无需你手动设置,而这一步也是循环的起点。在完成将列表中的第一项赋给“项”之后,遍历列表块内部的循环体开始第一次运行,设短信收发器1的电话号码属性为“项”的值(111-1111),并发送第一条短信。
当执行完循环体的最后一行代码(让短信收发器1发送消息)时,程序将跳回到循环的起点,并自动将列表中的下一项(222-2222)设为占位符变量“项”的值,然后重复执行循环体中的两个块,即向电话号码222-2222发送短信“想念你!”。然后程序再次回到循环的起点,并将“项”的值设为列表中的第三项(333-3333),执行第三次重复操作,发送第三条短信。
当列表中最后一项被处理完毕时,遍历列表的循环将终止。用编程术语来说,程序将跳出循环,这意味着程序计数器将继续下移来处理遍历列表块下面的代码块。在本例中,遍历列表块之后没有其他代码块,因此整个事件处理程序结束。
代码的可维护性
在最终用户看来,无论是遍历列表方法,还是暴力复制粘贴法,在最终结果上并无分别。但是对于程序员来说,遍历列表方法使得代码具有更好的可维护性:即使数据(如电话簿)发生变化,也不会影响程序的正常运行。(可维护性的重要特征是程序逻辑与数据的分离。——译者注)
首先,代码的可维护性使得对软件的修改更加容易,而且不会在程序中引入漏洞。使用遍历列表方法时,一旦需要修改短信接收人,只需要对列表变量进行修改,而无需对程序的逻辑(事件处理程序)做任何改动。相反,采用暴力复制粘贴法时,如果需要添加新的短信接收人,就需要在事件处理程序中添加新的代码块。任何时候,只要你改动了程序的逻辑,都会增加程序出错的风险。
同样,遍历列表方法可以适用于多种数据来源。现实中的绝大多数应用都着眼于动态数据的处理,很少有像上述例子中将三个固定电话号码直接写入代码的情况。还是以群发短信应用为例,假设电话号码的来源是动态的,可能是用户的输入,也可能来自于网络,或者来自于手机内部的通讯录。在这种情况下,你别无选择,只能使用遍历列表方法,因为在你编写程序的时候,根本无法知道会有哪些号码,因此也就无从采用暴力复制粘贴法。
显示列表
显示列表项最简单的方式就是将列表变量直接设置为标签组件的显示文本属性,如图20-4所示。
这样做的结果是,列表项在标签中显示为一行,列表项之间以空格分隔,整个列表被一对括号包围:(111-1111 222-2222 333-3333)。
这些号码可能显示为单行或多行,这取决于号码的多少。最终用户能够看到这些数据,也能把它们当作电话号码,但这样的显示方式很不美观。更常用的方法是,将列表项分行显示,或者至少用逗号分隔。
为了恰当地显示列表,需要将每个列表项转换为一段带格式的单独的文本。文本对象通常由字母、数字、标点符号组成,但也可以包含特殊的控制字符,它们是一些不可见的字符,如tab被表示为\t(关于控制字符的更多内容,请查阅文本表示的统一码[Unicode]标准:http://www.unicode.org/standard/standard.html )。
为了逐行显示我们的电话号码列表,需要一个换行符“\n”。当“\n”出现在一段文本中时,意味着“在下一行显示它后面的东西”。因此文本对象“111-1111\n222-2222\n333-3333”将显示为:
111-1111
222-2222
333-3333
要构造出这样的文本对象,需要用到遍历列表块,将每个列表项的后面加上换行符,再将其设置为电话簿标签的显示文本属性,如图20-5所示。
我们来跟踪一下这些块的作用。在第15章中讨论过在程序运行过程中跟踪变量及属性值变化的相关内容,在遍历列表块中,我们考虑每一次迭代之后,变量及组件的属性值。所谓一次迭代,就是遍历列表块执行一次循环。
在遍历列表之前,先设电话簿标签的显示文本属性为空;从遍历列表块开始,程序会自动将列表的第一项赋给占位符变量“项”。然后将电话簿标签当前的显示文本、\n以及“项”连接起来,再将其设为电话簿标签的显示文本属性。这样,在完成遍历列表的第一次迭代之后,相关的变量及属性值如表20-1所示。
表20-1 第一次迭代后的变量及属性值
项 | 电话簿标签的显示文本属性 |
---|---|
111-1111 | \n111-1111 |
此时,遍历列表块中的循环体已经执行完毕,程序进入第二次迭代,将占位符变量设置为下一个列表项(222-2222),并重复执行遍历列表块内部的代码块:将电话簿标签的原有显示文本属性值(\n111-1111)与“\n”及“项”(此时是222-2222)连接起来。第二次迭代后,变量及属性值如表20-2所示。
表20-2 第二次迭代后的变量及属性值
项 | 电话簿标签的显示文本属性 |
---|---|
222-2222 | \n111-1111\n222-2222 |
此时,将占位符变量设置为列表中的第三项,第三次重复运行遍历列表块内部的代码块。在完成最后一次迭代后,最终结果如表20-3所示。
表20-3 第三次迭代后的变量及属性值
项 | 电话簿标签的显示文本属性 |
---|---|
333-3333 | \n111-1111\n222-2222\n333-3333 |
每一次迭代完成之后,标签中都会新增一个电话号码,因而标签也变得更长一些(表现为新增一行)。在遍历列表块执行完成后,电话簿标签的显示结果如下:
111-1111
222-2222
333-3333
条件循环
与遍历列表循环相比,条件循环(只要满足条件...就循环执行...)稍显复杂,但条件循环的优势在于它的通用性:遍历列表只能针对列表执行循环,而条件循环可以为循环的执行设定任何条件。
在第18章中我们学习了程序中的决策,一个条件判断可能有两种结果:真或假。在条件循环块中也包含了一个像“如果...则”块一样的条件判断。当条件循环中的判断结果为真时,程序将执行条件循环块内部的代码,执行完毕后再跳回到循环起点,并再次进行条件判断。只要判断结果为真,条件循环块内部的代码就会重复执行。当判断结果为假时,程序将跳出循环(如同遍历列表循环一样),并继续执行条件循环块下面的代码块。
条件循环应用举例:公式计算
这是一个使用条件循环块执行重复操作的例子:你能猜到图20-6中的这些块在做什么吗?找到答案的方法之一,就是按照程序的执行顺序,跟踪每个变量及属性的值(关于程序跟踪的更多内容参见第15章)。
当变量加数的值小于等于变量最大数时,条件循环中的块将重复执行。在这段程序中,最大数的值等于最终用户在界面上输入的数字(最大数输入框的显示文本属性),假设用户输入了3。当程序运行到条件循环块时(循环的起点),程序中的变量如表20-4所示。
表20-4 即将开始条件循环时变量的值
最大数 | 加数 | 和 |
---|---|---|
3 | 1 | 0 |
在第一次循环中,条件循环块判断:加数值小于等于(≤)最大数吗?第一次判断的结果是真,于是执行循环中的块:设变量和的值为它现在的值(0)加上加数的值(1),然后加数值递增1。第一次循环之后,各变量的值如表20-5所示。
表20-5 第一次循环结束时变量的值
最大数 | 加数 | 和 |
---|---|---|
3 | 2 | 1 |
在第二次循环中,继续判断:加数小于等于最大数吗?结果仍然是真(2≤3),因而循环内部的块再次运行。变量和等于它自身(1)加上加数(2),加数继续递增。第二次循环结束时,各变量的值如表20-6所示。
表20-6 第二次循环结束时变量的值
最大数 | 加数 | 和 |
---|---|---|
3 | 3 | 3 |
程序再次返回到条件判断,这次的结果仍然是真(3≤3),于是循环内的块第三次运行。现在和的值为它自身(3)加上加数(3),结果为6;加数递增到4,如表20-7所示。
表20-7 第三次循环结束时变量的值
最大数 | 加数 | 和 |
---|---|---|
3 | 4 | 6 |
在完成第三次循环之后,程序再次返回到条件判断:加数小于等于最大数?或4≤3?,此时结果为假,因此循环内部的块不再执行,事件处理程序运行完成。
现在你该知道这些块的作用了吧?它们在做一个最基本的数学运算:求和运算。针对用户输入的最大数,程序将给出从1到最大数之间所有自然数之和。在这个例子中,我们假设用户输入了3,因此求和的结果是6;如果用户输入4,最后的结果应该为10。
小结
计算机擅长做重复的事情。想想看,所有的银行账户都要核算利息,每个学生的成绩都要折算成平均绩点,而日常生活中更是有不计其数的重复任务等待计算机去完成。
本章讲解了App Inventor中的两个循环块。遍历列表块适合于针对列表中的每一项实施一组相同的操作。我们特别强调了使用遍历列表块的优势,它可以针对列表的结构编程,或者说对抽象的数据编程,而不必在意列表中的具体数据。这样的程序具有更好的可维护性,而且,如果数据是动态生成的,那么就必须使用遍历列表块。
与遍历列表循环相比,条件循环具有更好的适用性:既可以处理单个列表,也可以同步处理两个列表,还能进行公式计算。在执行条件循环时,只要条件判断的结果为真,循环内部的块就会顺次执行;在循环内部的块运行完成后,程序将返回到循环的起点并重新进行条件判断,直到判断的结果为假,循环结束。