控制循环
这里会从以下几个角度深度讨论循环结构的使用规范
- 循环的种类
- 如何用好break
- 如何组织良好的循环结构
1.循环的种类
这里列出的是宽泛的概念:
-
计数循环:
并不是说这个循环用来计数,而是说这个循环执行的次数是确定的,是已知的。 -
连续求值的循环
当然也不是说用来连续求一个数的值,而是说循环的执行次数是不确定的,未知的。之所以称之为连续求值是因为,它在每次迭代时会检查是否应该结束循环。 -
无限循环
一旦启动就会一直执行下去,比如一些守护进程,服务器应用等。 -
迭代器循环
遍历容器类里的每一个元素。
当然在Java里循环大概只有四种:
- for循环
- while循环
- do-while循环
- foreach循环
关于while循环
while循环是一种非常灵活的循环,它既可以作为计数循环,又作为连续求值循环,更能作为无限循环。
while循环在每次循环时只进行一次终止检测,只不过根据终止检测在循环执行前和终止检测在循环执行后不同,while循环又分出两种,多出来的这一种在Java中就是do-while循环。
2. 如何正确的使用break
也有人认为break和goto一样,应该尽量避免使用,但是我觉得如果放在一个合适的if块(按照之前我们学来的标准的if块)中,依然可以很好地解释程序本身。
如在while循环中出现如下语句
if( !inputFile.EndOfFile() && moreDtaAvailable ){
break;
}
可以很清楚的了解到代码的意图:当文件读到结束时或者没有其他可用文件时,退出循环。
除此之外,就是好好使用注释来说明你的代码了。
3.如何组织良好的循环结构
下面的话很重要,事关如何组织良好的循环结构:
如果可能,我们应该首先吧循环看做一个黑盒子,我们本不应该过度地关心循环内部,要把循环内部当做一个子程序看待,这就要求我们必须尽可能地把控制卸载循环体外。并且把循环体执行的条件表述清楚。
外围程序只知道它的控制条件,却不知道它的内容。
总结上面两句话:一定要让控制语句和循环体分明,两者互不干扰,互不牵扯。并且,我们应该是可以通过阅读控制语句进而了解整个循环结构所做的事
下面这些技巧的目的归根结底都是为了减少错误发生的可能。
循环可能出现的问题可以归结为下面之一:
- 忽或错误地对循环执行初始化
- 忽略了对累加变量或其他与循环有关的变量执行初始化
- 不正确的嵌套
- 不正确的循环终止
- 忽略或者错误地增加了循环变量的值
- 不正确的循环下标访问数组元素等
减小出错可能的思路有以下两点:
- 尽可能简化影响该循环的各种因素的数量。
- 把循环体当做子程序不去关注而只关心外围的控制条件。
下面我将会从循环在执行过程中的几个时机切入:
-
进入循环
-
把初始化代码紧放在循环前
不仅可读性更强,修改的时候也便于一起修改。 -
使用while(true)表示无限循环
这是一种被普遍认可的标准写法,当然for( ; ; ;)
也能达到同样效果。 -
如果可能,请尽量使用for循环
for循环将代码的控制语句放在了一起,更易于阅读和维护。 - 在while循环更实用的时候,不要使用for循环
-
把初始化代码紧放在循环前
-
执行循环体
-
使用
{}
把循环体中的语句括起来
就算循环体只有一行,也要括起来 - 避免空循环
-
把循环内务操作要么放在循环的开始,要么放在循环的末尾
循环内务操作是指i +=1
或者i++
这样的表达式,它们的主要任务是为了控制循环,所以不要把它们穿插在代码的中间,而是在开头或结尾处统一操作它们。 -
一个循环只做一件事
仅仅靠循环可以同时做两件事这一事实,是无法充分证明这两个应该是放在一起的。循环应该和子程序一样,每个循环只做一件事,并且把它做好。
-
使用
-
退出循环
-
设法确认循环能够被终止
这是一个基本要求。 -
使循环终止条件看起来很明显
只要你没有随意地修改循环下标,或者用break蹦来蹦去,那么这个循环的终止条件就是明显的。
关键就在于把控制语句都放在一个地方。 -
不要为了终止而任意修改循环下标
如第二条所提到的,循环下标就应该在控制语句部分进行操作,而不是在循环体的中部,或者是控制语句以外操作。第一这很业余(书中原话。XD),第二这么做导致终止条件不再明显(违反了第二条),第三明显这么做程序会更容易出错。 -
避免出现依赖于循环下标最终取值的代码
循环下标的最终值是不确定的,有时候循环可能会提前终止,也可能会因为实现的不同而不同。无论如何更合适的做法是在循环体内的某个适当的地方把循环下标的最终值赋值给某个更易读的变量。
5.使用安全技术器
定义一个全局变量SAFETY_LIMIT
,一但循环次数超过此值,就应该提前终止或处理该异常情况, 但是不是所有循环都需要这样一个安全技术器。
-
设法确认循环能够被终止
-
关于提前退出循环(3、4条比较重要)
-
使用break和continue来提前退出循环
-
小心那些有很多break散布的循环
如果你在一个循环中使用了多个break,那么你就应该考虑能不能用一系列循环来代替这个含有多个出口的循环,这样可以使程序表达更清晰 -
在循环开始处用continue进行判断
这是一个我之前没见过的技巧,它可以让你在执行本次循环提前先进行判断,是否需要执行本次循环,虽然使用if进行判断也可以,但是会导致整个循环体层次缩进。 -
使用带标号的break
尽可能是自己的代码严谨,总是使用带标号的break是一个好习惯,同时它也允许你更加灵活地跳出多个结构,使代码可读性更强。 -
使用break和continue时要小心谨慎
如开头所言,我们本应该将循环体当做黑盒,但是使用break和continue已经打破了这一假设,也就是说循环体已不再是黑盒,我们就不得不去仔细阅读循环体的内容,否则我们将无法理解循环是如何控制的。 因为break和continue本身就是控制语句,而现在,控制语句出现在了循环体中
所以综上所述,我们得出如下结论(原文引用):
-
如果你没有考虑过各种其他的方案,不到万不得已,不要使用break和continue。因为我们无法确定它们是好还是坏。
如果你不能证明使用break或者continue的正当性,那就不要用它们
下面是上述的案例代码
//3.在循环开始处使用continue判断
while(){
if(//条件不满足){
continue;//直接越过本次循环
}
//循环体语句
...
}
//4.使用带标号的break
waiceng:
for (int i = 0; i <= 10; i++) {
System.out.println("外层循环i="+i);
for (int j = 0; j <= 10; j++) {
System.out.println("内层j="+j);
if ( j == 5){
break waiceng; //直接连跳两层也就是说将直接跳出最外层的循环
}
}
}
//运行结果
外层循环i=0
内层j=0
内层j=1
内层j=2
内层j=3
内层j=4
内层j=5
-
端点检查★★★★
是否愿意执行这种检查,这是高效程序员和低效程序员之间的一项关键差别。
需要注意三种情况:- 开始的情况
- 任意选择的中间情况
- 最终情况
确保不会出现差一(off-by-one)错误,高效的程序员既会在脑海里进行模拟,也会手动执行运算。(手边备个演草纸很正常,程序员也不一定只需要键盘,偶尔也需要写写画画)
低效的程序员会随意地做一些实验,智导他们找到了一种看上去能工作的组合。如果某个循环没有按照想象的那样去工作,低效的程序员可能会把
<
改成<=
.如果还不行,他们就会把循环下标加1或者减1。这样做可能最终能碰出正确的组合,也可能把原有的错误改成了另一个更微妙的错误,然后耽误更多的时间。即使这样随意开发能够产生一个正确的程序,这些程序员也不明白为什么这个程序是正确的。
你可以通过在头脑中模拟和手工运算获益多多。这种智力训练带来的好处是:在最初的编码阶段少犯错误,在调试阶段更游刃有余更快地找出错误,从而整体上更好滴理解应用程序。它意味着:你能够真正理解你的代码是如何工作的,而不是瞎猜。
md这不就是说我的吗,我就是这样干的。。。。XD
4.如何正确使用循环变量
- 用整数表示数组和循环的边界
- 在嵌套循环中使用有意义的变量名来提高其可读性
具体参考之前笔记-变量名的命名。 - 用有意义的名字来避免下标串话
别再习惯用i、j、k
,完全可以使用比它们更加有意义的变量名。 -
把循环下标变量的作用域限制在本循环内
这个对Java来说并不难,Java允许直接将循环变量定义在循环头部。
5. 循环应该有多长
- 循环应该尽可能地短,以便能一目了然
很少会超过20行代码。 - 嵌套应该控制在3层以内
研究表明,超过三层后程序员对循环的理解就会极大降低。 - 如果你的循环需要处理很多事,试着把它们放到一个单独的子程序里
要点 key point
- 循环很复杂。保持循环简单有助于调高你代码的可读性
- 保持循环简单的技巧包括:避免使用怪异的循环、减少嵌套层次、让入口和出口一目了然、把内务代码操作代码放在一起。
- 循环下标很容易被滥用。因此命名要准确,并且要把它们各自仅用于一个用途。
- 仔细地考虑循环,确认它在每一种情况下都运行正常,并且在所有可能的条件下都能退出。