购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

7.2 While循环

在编写For i=A To B … Next 这种循环结构时,我们必须写清楚A和B这两个数字,换句话说,在编写程序时必须事先知道预期的循环次数(B – A+1)。例如在案例7-3中,我们必须事先知道表格数据的起始行是第4行及结束行是第7行,然后才能写出代码“For i=4 To 7”。

但是在大多数情况下,我们在编写 VBA 程序时根本无法知道将来工作表中会包含多少行数据,更何况用户还可以随时在表格中执行添加或删除操作。无法确定数据的起止行号(A和B),就无法书写For i=A To B语句,也就不能使用循环结构处理问题。所以,VBA中的For循环虽然简单易懂,但在实际应用中还是存在一定的局限性。

幸运的是,VBA还为我们提供了另一个非常重要的循环结构——While语句及其各种变形,可以完美弥补For语句的上述不足。

7.2.1 Do While循环的基本用法

与For语句不同,While语句不要求指明循环的次数,但是要求指明“在什么情况下应该继续循环”,或者说“在什么情况下应该停止循环”。这就是为什么使用“While”这个单词作为这个循环的关键字,因为“While”的含义就是“当……时”,即“当符合某个条件时,请执行循环体”。

While循环有多种写法,其中最常用的就是 Do While … Loop,格式如下:

从上面的格式可以看到,该循环结构以“Do While”为起始,到“Loop”结束,中间的代码都属于重复执行的循环体。在关键字While后面可以书写一个条件表达式,比如“Do While i < 5”,就像在If语句中使用的判断表达式一样。如果这个表达式成立,那么就会进入循环体执行其中的代码,直至遇到关键字“Loop”会再次回到第一句“Do While”上。此时再次判断While后面的循环条件是否成立,如果仍然成立就再次执行循环体。反之,如果在刚才的执行过程中,由于某些变化使得循环条件不成立,那么VBA就跳过循环体并停止循环,直接执行Loop后面的其他语句。下面以案例7-4中的需求为例,具体演示一下Do While的使用方法。

案例7-4: 如图7.7所示,工作表中含有某个部件的美元报价清单。请编写程序将全部数据转换为人民币报价(汇率:1美元=6.5人民币),预期效果如右图所示。

图7.7 案例7-4的原始数据及预期效果

相信读者都能够很熟练地使用For循环解决这个问题,只要用一个循环变量(如i)代表行号,并让它从4递增到7即可。那么怎样使用Do While循环实现同样的功能呢?关键思路就是:把“i应当从4开始到7结束,每变化一次就执行循环”这个规则,转换为“当i符合某个条件时就执行循环”这种叙述方法。具体参考代码如下:

可以看到,这个程序中的Do While循环与“For i=4 To 7”这种写法具有完全相同的功能,都使用了一个变量i代表行号,并且在每次循环中都要将i的数值增加1。但是很明显,使用Do While循环在写法上更加烦琐一些,因为它不能像For语句那样直接通过“=”与“To”指定i的起止数值,而是需要书写单独的语句“i=4”“i < 8”和“i=i+1”来完成这些工作。读者可以思考一下(思考即可,千万别急着在计算机上试验):如果删除其中某个语句,比如“i=i+1”,程序运行后会是怎样的结果?答案会在下一小节揭晓。

如果Do While循环写起来比For循环麻烦,为什么VBA还要提供这种语句呢?奥妙就在于While后面的这个“循环条件”——灵活地设计这个条件表达式,我们就可以随心所欲地实现各种循环结构。

例如,重新考虑案例7-4的需求。假如在编写VBA程序时无法确定工作表中供应商的数量,因为用户可能随时向其中添加新的供应商或删除已有供应商,使用 For 循环来完成这个工作就不太方便。但是,只要该表格各行数据记录都是连续存放的,中间不允许存在空白行(这也是最常见的格式要求),那么使用Do While循环就可以轻松解决问题。因为我们可以把循环条件设置为“当第i行的第2列不是空白单元格时,就执行循环体”,这样一旦第i行第2列单元格(供应商名称)的内容为空白时,循环就会自动结束。

按照这个思路,可以使用下面的代码来解决“行数未知”的问题。

这段代码与前面代码的写法几乎相同,只不过在Do While一句中修改了循环条件。这样当第一次执行到Do While语句时,由于变量i刚刚被赋值为4,所以本句的含义就是“当第4行第2列的内容不等于空字符串时执行循环体”。由于表格中B4单元格内容为第一个供应商的名字“1”,不是空字符串,所以符合条件并执行循环体。

接下来,在循环体中变量 i的数值因增加1而变成了5,于是通过 Loop 语句再次回到 Do While一行时,循环条件就变成了“当第5行第2列的内容不等于空字符串时执行循环体”。由于B5单元格的内容是“2”而不是空字符串,所以仍然符合条件,再次执行循环体……直到处理完第7行数据且让i从7变成8后,循环条件变成了“当第8行第2列的内容不是空字符串时”。而在工作表中,B8单元格没有任何内容,所以B8单元格的内容就被认为是空字符串(或数字0),因而循环条件不再成立。这样当i等于8时,Do While循环自动停止,程序转到Loop后面的语句继续执行,直至结束。

可以看到,在这种写法中我们只需要指定表格数据的起始行号(i=4),完全不必提及表格数据的最后一行在哪里,因为Do While循环会从起始行开始逐行寻找下去,直至遇到第一个空白行为止。通过这种方式,我们就可以轻松应对数据规模无法确定的工作表。

更方便的是,Do While结构中的循环条件与If结构的判断条件一样,可以使用全部关系运算符,以及And、Or和Not等逻辑运算符,组合出各种复杂的条件表达式,比如下面的写法也是完全符合要求的(该段代码仅用于格式示例,与案例7-4无关):

7.2.2 While循环结构的初学者陷阱

与之前介绍过的其他语句一样,While 循环结构在使用中也有很多陷阱,很容易让初学者犯错误。

1.误入死循环

在讲解For语句时,我们已经介绍过“死循环”的概念和原因。对于For语句来说,只要不在循环体内修改循环变量的数值,就基本不会写出死循环代码。但是对于While语句来说,只要循环条件能够永远成立,就会造成死循环,因而出现这个问题的概率要高出很多。

最典型的一种情况,就是在逐行扫描工作表时忘记对行号变量执行“增1”的操作。比如在案例7-4的代码中,如果在循环体里面忘记书写“i=i+1”,就会出现这种情况。

上面这个程序对案例7-4的需求进行了一点修改:在根据C列单元格中的数据计算出人民币价格后,不是重新回到C列中,而是把人民币价格写入D列单元格。但是由于忘记对i执行增1操作,所以在每次执行循环体时,i的数值都是4,因而计算的都是第4行数据。更重要的是,每次从Loop跳回Do While执行判断时,循环条件都是“当第4行第2列不为空字符串时执行循环体”。由于B4单元格的内容一直是“1”,所以该条件永远成立,这个循环就会永远执行下去,形成一个死循环。

另外一种更加直白的死循环,就是直接写出一个永远成立的循环条件,比如一些“老派”的C语言程序员在设计侦听程序时愿意使用下面的方法:

这个写法堪称程序设计中“暴力美学”的经典代表之一。那么运行这种循环的程序是否真的无法中断呢?答案也不尽然,后面要讲解的“跳转语句”就是专门应对这种情况的,这里暂不赘述。

2.循环变量并非必需

从刚刚演示的死循环代码中还可以看到:While 循环的循环条件可以任意书写,并非必须使用一个循环变量。这一点与For循环有着明显的区别,同时也是Do While循环比For循环更加灵活强大的原因。

但是很多初学者在使用Do While循环时,总是挖空心思想为其安排一个循环变量,即使这个程序事实上完全用不到循环变量。在本书后面章节中会经常看到这样的例子,所以请读者提前理解:For循环使用循环变量来控制循环次数,但是While循环根本不关心循环次数,只关心让循环继续的条件。所以While循环并不需要循环变量。

3.空单元格不是空格

像案例7-4演示的那样,我们经常会使用“Cells(i,1)<> “””这种方式,通过判断某行单元格是否为空字符串来决定是否继续While循环。但是在实际工作中,常有一些单元格看上去一片空白,实际上却包含了多个空格字符。

在讲解字符串时我们专门讲到:空格字符也是字符,与不包含任何字符的空字符串完全不同。所以,尽管空单元格常被简称为“空格”,但它的含义是“内容为空字符串的单元格”,与“内容为若干空格字符的单元格”存在本质区别,二者不能混淆。

所以在案例7-4中,假如第8行B列单元格的内容是两个空格,那么从肉眼上看我们仍然会认为它是一个空白行,于是认为程序会在执行到第8行时自动停止(因为循环条件是“第i行不为空”)。但是在实际执行程序时,由于B8单元格的内容并非 “”,此时的循环条件Cells(i,2)<> “”仍然成立,进而继续执行循环体并对第8行也进行计算,效果如图7.8所示。

图7.8 案例7-4代码及在B8单元格输入空格后的运行结果

B8单元格含有两个空格,于是在程序运行后C8单元格也被执行了计算,将其原来的内容(空单元格内容被默认为数字0或空字符串)乘以6.5,得到新的结果(数字0)并显示。

为了避免这种肉眼难以发现的错误,我们可以使用“字符串函数”来解决这个问题,即把Do While一句改为“Do While Trim(Cells(i,2))<> “””。这样即使单元格的内容是由多个空格构成,最终也会被转换为空字符串统一处理。关于“函数”的概念及这种写法的原理,本书会在相关章节中详细介绍。

7.2.3 Do While循环的典型应用

从功能上讲,VBA中的Do While循环完全可以替代For循环,利用Do While循环不限定循环次数的特点,还可以实现更多For循环不易实现的功能,比如下面这两种十分常用的操作。

1.查找指定内容所在的单元格位置

由于用户可能随时在 Excel 工作表中插入或删除行列,所以在编写程序时往往不能确定数据所在的行列位置,也就无法使用VBA自动处理这些数据。案例7-5就是这样的例子。

案例7-5: 年末,各地区子公司分别上交了一份季度销售报表,并由总公司财务部门拷贝到同一个Excel文件的不同工作表中(每个子公司报表占用一个工作表),准备分别进行汇总计算。但是由于在拷贝过程中没有注意格式,每个工作表中的数据起始列都有所不同。请编写一个VBA程序,使之无论计算哪个工作表,都能够准确定位到该表中的销售数据并进行汇总操作。图7.9给出了其中两个工作表的示例和预期效果。

在图7.9左侧的两个工作表中,两个子公司的起始列(“季度”所在列)分别为C列和B列,而“总计”列则分别位于G列和E列。此外,华北地区数据表中共包含三列数据,而华东地区数据表中只包含两列数据,汇总范围也不相同。

图7.9 案例7-5不同子公司工作表及预期效果示例

我们知道,类似这种二维汇总的问题都可以使用本章前面介绍的双重 For 循环结构解决。具体来说,先用外层循环扫描第4行到第7行的每行(因为每个子表格都是从第4行开始,并且包含四个季度的数据),然后用内层循环扫描从“季度”后面一列(第一个城市所在的列)直到“总计”前面一列(最后一个城市所在的列)的数据。只要在这个双重循环中使用一个累加器变量,就可以对每行数据进行汇总。

但是问题在于,内层的For 循环要求事先知道“季度”和“总计”两列的列号,否则无法指定For语句中循环变量的起止数值。那么怎样能够得知这两个列号呢?这就是Do While循环的用武之地,请见下面的参考代码及注释。

这个方案的关键之处,在于两个看起来没有执行任何操作的Do While循环。第一个Do While循环只做了一件事情:只要第3行的第colSeason列不是“季度”二字,就将colSeason增加1,并重新检查。随着colSeason数值不断增加,直到第3行第colSeason列显示“季度”二字所在的单元格,循环条件“Cells(3,colSeason)<>“季度””不再成立。这时 Do While 循环终止,而变量colSeason中的数值就是这列的列号。换言之,我们找到了“季度”栏所在的列号并把它保存到了变量colSeason中。

同样的道理,我们又利用第二个 Do While 循环找到了“总计”二字所在的行号并存入变量colSum中。与之前略有区别的是,寻找“总计”列并不是从第1列开始查找,而是从第colSeason+1列开始。因为已经找到了“季度”二字的列号,而“总计”列一定出现在“季度”列的后面,所以完全没有必要再从第1列开始搜索,直接让colSum=colSeason+1,然后进入Do While循环逐次增一即可。

这样,只要工作表第3行中确实依次存在“季度”和“总计”两个单元格,两个Do While循环就都可以正常结束并将它们的列号保存到colSeason和colSum中。接下来使用双重For循环就可以实现汇总计算。与案例7-2不同的是,内层循环在扫描各列时,起止列号不再是固定的数字,而是 colSeason+1与colSum-1。

这种除了将循环变量增加1不执行任何实质性操作的循环,常被形象地称为“空循环”(注意:空循环不是死循环,后者可以执行很多操作,但永远不会结束)。在程序设计中,使用空循环定位到指定内容是一个很常用的基本技巧。尽管Excel VBA中还提供了很多简单高效的方法(如Find方法)可以查找单元格,但是在处理一些特殊需求,或者需要在工作表之外执行其他查找定位操作时,空循环仍然是不二之选。所以请读者仔细阅读和理解这部分内容。

另外必须说明的是,本例中的参考代码其实存在一个重大缺陷:假如用户提供的表格的第3行中没有“季度”或“总计”两列,那么Do While循环的循环条件就永远无法成立,会一直循环查找下去。不过这也不会导致死循环,因为Excel工作表的行数和列数均存在上限,比如在Excel 2016中最大行号是1048576,最大列号是16384。所以当变量colSeason增加到16385时,Cells(3,colSeason)就已经超出了 Excel 表格的最大范围,指向了一个不存在的单元格。这时由于无法找到这个单元格,VBA会报出一个编号为1004的运行时错误警告,提示“应用程序定义或对象定义错误”,如图7.10所示。

图7.10 运行时错误“1004”警告框

所以一旦遇到这个错误提示,就应想到是否是某个Cells引用了不存在的单元格(比如超出最大行列号,或者引用第0行、第0列等),进而检查代码的逻辑问题。

不过对于普通用户来说,在运行 VBA 程序时如果遇到这个警告框,一定会感到十分紧张和不解。那么能否让这个 VBA 程序在工作表中没有“季度”列时也不引发异常警告,或者弹出一个我们自己设计的友好提示,例如“请检查第3行中是否有‘季度’或‘总计’单元格”呢?答案是肯定的:有很多编程技巧可以解决这个问题。本章最后就会结合Goto语句介绍其中很常用的一种方式,更多方法则会在讲解相关内容时介绍给大家。

2.与其他结构嵌套使用

各种循环和判断结构都可以相互嵌套,完成复杂的程序逻辑,Do While循环也不例外。比如对于前面所有使用多重循环的案例,我们都可以把其中的For循环改写成Do While循环,用多重Do While循环实现相同的功能。事实上,由于For循环在处理已知循环次数的问题时语法更加简单(不需要手动编写i=i+1等语句),而Do While循环则能轻松搞定未知循环次数的问题,所以我们经常将两者搭配嵌套,以便高效完成任务。案例7-6就是一个例子。

案例7-6: 在图7.11上图所示的工作表中显示了六个部门的员工名单,并用不同背景色标记出了不同部门,以呈现简单的柱形图效果。请编写程序统计每个部门的总人数,并将其显示在紧邻该部门最后一个员工姓名的单元格中,预期效果如图7.11下图所示。

图7.11 案例7-6的原始数据和预期效果

这个案例的特点是:行数确定(公司部门一般不会轻易增减),但每行数据的列数不固定。因此适合使用For循环扫描各行,并使用Do While循环扫描各列,参考代码如下:

这段代码也使用了“空循环定位”技巧,用于找到每行的第一个空单元格,也就是案例需求中描述的“紧邻该部门最后一个员工姓名的单元格”。由于每个姓名占用一列,且第一个姓名位于第3列,所以第一个空单元格的列号减去3就是总列数,也就是员工总数。

根据不同语句(For、While、If … Then、Select … Case)的功能和特点,通过嵌套组合实现复杂逻辑,是每个编程初学者必须熟练于心的基本功。本章列出的案例和参考代码属于最典型的基本用法,希望读者能够读懂并做到独立编写。

7.2.4 While循环的各种形式

Do While…Loop循环并不是唯一一种While循环,VBA还提供了很多其他形式的While循环。尽管这些循环与Do While … Loop循环没有本质区别,可以相互替代,但是在某些情况下选择不同形式确实可以简化编程。下面就是VBA支持的各种While循环。

1.While…Wend循环

While…Wend循环与Do While … Loop循环的功能完全相同,只是不需要写“Do”关键字,同时将“Loop”换成“Wend”(“While End”的缩写)。比如下面两段代码的运行流程和结果是完全一样的。

既然While…Wend循环的效果与Do While…Loop循环效果相同,而且还少写了一个关键字,为什么Do While…Loop循环却更加常用呢?这是因为Do While…Loop循环允许使用“Exit”随时跳出循环,而While…Wend循环却不能使用Exit语句。此外,Do While … Loop循环还可以轻松变形为Do … Loop While循环。所以尽管While … Wend循环看上去更像“正宗”的While循环,但在实际编写程序时使用更多的还是Do While循环。

2.Do While … Loop循环和Do … Loop While循环

如果把Do While … Loop循环中的While放在最后一行,也就是Loop的后面,就得到了Do …Loop While循环结构,意为“请执行循环体,当符合某个条件时”。

Do…Loop While循环的含义看起来与Do While…Loop(当符合某种条件时,请执行循环体)没有区别,但对于计算机这种死板的机器而言却有很大的区别。事实上,二者的区别就在于“判断循环条件”与“执行循环体”的顺序上:Do While … Loop循环会先判断是否符合条件,再决定是否执行循环体;而Do … Loop While循环则先执行一遍循环体,再判断是否符合条件。

从另一个角度讲,Do … Loop While循环能够保证循环体至少被执行一次,而Do While …Loop循环则有可能一次都不执行。比如下面这段代码,虽然i<5这个循环条件从程序一开始就不成立,但是Do … Loop While循环仍然会执行一次,修改了第5行单元格的内容。

Do … Loop While循环虽然很少使用,但这种“先斩后奏,至少执行一次”的特点,在某些场合十分有用,本书后面在讲解一些复杂技巧时就会用到这个结构。

3.Do Until … Loop循环和 Do … Loop Until循环

与Do While … Loop循环相对应的,VBA还提供了一个Do Until … Loop循环,意为“在符合条件之前,请执行循环”。换言之,由于将“While”(当……时)换成了“Until”(直到……时),循环的条件就发生了变化,变成了“如果尚不符合条件则执行循环,一旦符合条件就停止循环”。下面两段代码就分别使用这两种循环实现了同样的功能。

左边代码的含义是“当i小于等于5时继续循环”,右边代码的含义则是“在i大于5之前继续循环”,其实二者表达的是相同的意思。事实上,如果想把代码中的“While”和“Until”互换,只需要把循环条件反写一遍,或者直接在前面加上一个逻辑运算符“Not”就可以了。比如前面的代码还可以写成:

所以,当我们希望编写“一旦符合某个条件,请马上终止循环”的代码时,就可以使用 Do Until … Loop循环结构,这样可以直接把条件写到代码中,不需要再进行反写。

此外,与Do … Loop While循环一样,Until也可以放在Loop的后面,构成 Do … Loop Until循环结构。它的特点也是先执行后判断,循环体至少能够执行一次。 kf0wQaTSMZzjRUCi/8tIl624oey7irPIlICwp/a2uAQRVh8u399vez8VsnvX5Vgf

点击中间区域
呼出菜单
上一章
目录
下一章
×