16.2 循环的控制

文章目录
  1. 1. 循环"头"
    1. 1.1. 自从一个位置进入循环
    2. 1.2. 把初始化的代码紧放在循环前面
    3. 1.3. 用 while(true) 表示无限循环
    4. 1.4. 在适当的情况下多使用 for 循环
    5. 1.5. 在 while 循环更适用的时候,不要使用 for 循环
  2. 2. 循环体
    1. 2.1. 用{ }将循环中的语句包围起来
    2. 2.2. 避免空循环
    3. 2.3. 把循环内务操作放在循环的开头或结尾,不要放在中间
    4. 2.4. 一个循环只做一件事
      1. 2.4.1. 特殊情况的处理
  3. 3. 循环’尾’
    1. 3.1. 设法确认循环在任何情况下都能够终止
    2. 3.2. 使得循环终止条件看起来很明显
    3. 3.3. 不要为了终止循环而胡乱改动 for 循环的下标
    4. 3.4. 避免在循环终止后直接使用循环下标值
    5. 3.5. 考虑使用安全计数器
  4. 4. 提前退出循环
    1. 4.1. break 和 continue
    2. 4.2. 在 while 循环中考虑利用 break 语句而不是布尔标记变量
    3. 4.3. 小心那些大量充斥着 break 的循环
    4. 4.4. 如果语言支持,请使用带标号的 break 结构
    5. 4.5. 在循环开始处用 continue 进行判断
    6. 4.6. 使用 break 和 continue 时要谨慎
  5. 5. 去检查端点
  6. 6. 循环变量的使用
    1. 6.1. 用整数或枚举类型表示数组和循环的边界
    2. 6.2. 更加有意义的变量名
      1. 6.2.1. 如果循环嵌套了,使用更加有意义的变量名
      2. 6.2.2. 用有意义的名字避免循环下标串话
      3. 6.2.3. 什么时候避免 i、j、k ?
    3. 6.3. 把循环下标变量的作用域限制在该循环内
  7. 7. 循环应该多长
    1. 7.1. 循环要尽可能短,以便可以一目了然
    2. 7.2. 嵌套层次在 3 层以下
    3. 7.3. 把长循环的内容移到子程序内
    4. 7.4. 让长循环格外清晰

问题会出现的地方:

  • 初始化
  • 累加变量
  • 嵌套
  • 循环终止
  • 循环变量
  • 循环下标访问数组元素

应该把循环内部当成一个子程序看待,是一个黑盒;把控制尽可能放到循环体外,使得外围程序只知道控制条件,而不需要知道循环内的内容

note:上面不适用于 while(ture) - break 方法,因为退出条件放在了黑盒中

toExemplify-book

循环"头"

自从一个位置进入循环

??是说每次只需从循环头部进入就可以了吗?

把初始化的代码紧放在循环前面

[[就近原则]]

这会让你在修改代码的时候(如将这个循环放到更大的循环、移动到另一个子程序等等),不会忘记修改相应的初始化代码

用 while(true) 表示无限循环

普遍认为 while(true) 是标准写法,像 for( ; ; ) 也可以接受。但 for i = 1 to 999999999 这种假造无限循环的语句是很不好的

在适当的情况下多使用 for 循环

因为 for 循环的控制代码(初始化、判断条件、循环变量改变)都集中在一处,所以可读性更强,修改时也不会忘记某一地方。如果可以用 for 循环来替代其他类型的循环,就这样做。但要注意,是在恰当的情况下使用 for 循环,可参考[[这里]]

在 while 循环更适用的时候,不要使用 for 循环

这个就是对上一条的补充说明

使用 for 循环时的一种陋习:在 for 循环的循环头中塞入本属于 while 循环的内容(如并不控制循环进度的 housekeeping statements 内务语句)。这会产生一种误导,让人以为这些 housekeeping statements 也在控制着这个循环

toExemplify-book

解决办法是,将这些 for 循环改用 while 循环

循环体

{ }将循环中的语句包围起来

任何时候都这样做,就是只有一行循环体甚至是空语句。

增加括号不会增加运行时所需时间和存储空间,只会提高可读性和预防修改代码时出错

避免空循环

toExemplify-book

妈的,这个例子太好了!以前就觉得空循环的写法不妥

把循环内务操作放在循环的开头或结尾,不要放在中间

循环内务操作(housekeeping)是指像 i++ 或 j = j + 1 这样的表达式,它们的主要目的是控制循环(进行下一轮循环判断条件的准备)而不是完成循环工作

toExemplify-book

一个循环只做一件事

循环应该和子程序一样:一个循环只做一件事并把它做好

特殊情况的处理

假如在实际情况中,根据上一原则使用了两个或多个循环会导致效率低下(比单独写成一个循环的效率低),那么还是写成两个或多个循环,加上注释说明可以将它们合并起来提高效率,等测量数据显示这部分性能的确很重要时再去合并它们

循环’尾’

设法确认循环在任何情况下都能够终止

这是基本要求。在脑中模拟,考虑正常的情况、端点,以及每一种异常的情况

使得循环终止条件看起来很明显

正确使用 for 循环的情况下,终止条件是很明显的(就和循环头里指明的一致)。

使用 while 循环时,应该把所有的控制语句都放在 while 子句中,这样也会使得终止条件十分明显,关键在于把控制都放在一个地方(??)

不要为了终止循环而胡乱改动 for 循环的下标

这一条是上一条的补充。这样做其实会让人阅读代码时看错循环的终止条件。

一旦写好了 for 循环,就不要去试图更改、控制 循环变量;如果需要的话,请使用 while 循环来获得对退出条件更多的控制

避免在循环终止后直接使用循环下标值

这样做不好的原因:一,循环下标的最终取值可能和想象中不同(语言、实现不同造成影响;循环是否是正常终止);二,思考这个取值是多少,需要花费时间(还可能出错……)

更好且更具自我描述性的做法:新增一个变量,在循环体某个适当的位置把最终结果赋给它。

如多用一个布尔变量记录结果:

toExemplify-book

考虑使用安全计数器

安全计数器是指,额外增加一个 safetyCounter 变量,每次循环后都递增它进行计数,以此判断执行次数是不是在合理范围内

这样做的缺点是提高了复杂度(增多了一部分代码;在需要修改的时候也可能忘记修改这一部分),所以不会到处都使用,一般在关键的循环处使用即可

toExemplify-book

提前退出循环

break 和 continue

break:终止所在处的整个循环,使该循环正常退出

toExemplify-own

continue:不会让程序终止掉整个循环,而是让程序跳过这一次迭代过程中(循环体中) continue语句后面的部分,然后从该循环的下一次迭代的开始位置继续执行

continue 相当于 if( ) then { next iteration }

在 while 循环中考虑利用 break 语句而不是布尔标记变量

???

toNote 379页

小心那些大量充斥着 break 的循环

使用多个 break 不一定就是错的。只是如果一个循环包含很多的 break,就要当心了,因为有可能会误用了某个 break 而连不应该退出的部分都退出了

toExemplify-book

改用一系列的循环可能会更加清晰(而不是只用一个循环、用多个 break 作为出口)

如果语言支持,请使用带标号的 break 结构

Java 支持 labeled break,可以对任何在大括号里的代码段进行标号,使用 break 指定退出

toExemplify-book

在循环开始处用 continue 进行判断

????

toNote 381

使用 break 和 continue 时要谨慎

我们无法确定使用 break 和 continue 是好是坏

使用了 break,就不能再把循环看成黑盒了,阅读代码的人必须去读循环体才能知道循环是如何控制的(也就是终止条件变得不明显了)

建议:可以使用它们,但是要对可能产生的错误保持警惕(一般来说,错误的产生是因为终止条件发生了变化,程序员容易判断出错);如果没有有说服力的理由,那么就不用使用它们

去检查端点

在创建循环时,应该用头脑去模拟(甚至进行一些手工的计算)循环的情况

通过这样的训练,才能在最初的编码阶段少犯错、在调试阶段更快找出错误,以及在整体上更好地理解程序,而不是瞎猜

通常要考虑的情况:

  • 开始情况
  • 任意选择的中间情况
  • 最终情况

模拟时,应该确认不会出现任何 off-by-one 错误。如果循环中有复杂的计算,应该拿出计算器来手动检查一下

循环变量的使用

用整数或枚举类型表示数组和循环的边界

浮点数递增会有问题

toExemplify-book

更加有意义的变量名

如果循环嵌套了,使用更加有意义的变量名

一般来说,在简单的循环里使用 i、j、k 等变量名还可以接受。但如果有多层的嵌套循环(复杂度更高了),应该用更有意义的名字提高可读性,帮助理解

参考[[循环下标的命名]] 11.2节

toExemplify-book

用有意义的名字避免循环下标串话

用惯了 i、j、k 可能会导致下标串话(cross-talk)

toExemplify-book

什么时候避免 i、j、k ?

某个循环体内的代码多于两三行(……那基本都是啦),或者有增长的可能,或者位于一组嵌套的循环里 —— 都应该避免 i、j、k 作为循环下标、

把循环下标变量的作用域限制在该循环内

这样可以避免循环下标串话和其他在循环外部继续使用循环下标的危险做法

toExemplify-book

在 C++、Java 等语言中,可以在循环内部声明循环下标变量,这样就把它的作用域限制在循环的括号内了

但要注意,不同编译器的实现不同,它们对这种特性下的循环变量作用域的检查也可能不同(具体是什么意思??toExemplify-book

循环应该多长

循环的长度可以用代码行数或嵌套层次来衡量

循环要尽可能短,以便可以一目了然

一个标准是不超过显示器的宽度吧(这样也很长了);不过自从你有意识去编写简单代码,一般很少会写出超过 15~20 行的循环

toExemplify-book

嵌套层次在 3 层以下

循环的嵌套层次过多,会让人很难理解。

缩短的方法是:将某一部分提取为子程序或者将控制结构简化。(当然这是概念上的缩短,编译器还是会将它展开为原来的长度,只是这样做可以让我们更容易把握、分块去理解)

把长循环的内容移到子程序内

这条跟上面那条里的解决方法是差不多的。将循环体内的代码提取为子程序,再加以调用,可以帮助我们在整体上把握循环的运行结构

让长循环格外清晰

在短循环内可以适当使用 break、continue、多个出口和复杂的终止条件

但如果循环很长(即使已通过其他方法简化),那么应该让出口保持单一、退出条件清晰,来控制复杂度~~【就是“循环已经很长了,还搞那么复杂作死啊”的意思】~~