程序的流程控制-循环语句

从这节开始,你将会接触到编程中一个重要且强大的知识点 - 循环语句。

在此之前,若你想在程序中画一万个圆,只能用一个可怕的方式去做,写一万行 ellipse。哪些千方百计为了提高效率(偷懒)的语言设计者肯定不会允许这样的事发生。所以就有了循环语句。通过它可以让你最直观地感受计算机自动化的威力。

for循环

循环语句有多种,其中最常用的就是 for 循环。我们都了解 draw 函数是不断循环执行的。先从开头的第一句开始,由上至下执行直到最后一句,执行完一遍后,又会再从第一句开始重新运行。for 语句和它有点类似。写在它里面的代码都可以被反复执行。

它的语法结构是这样的:

	for(表达式1; 表达式2; 表达式3){
		循环体
	}

循环体中自然就是写我们希望被反复执行的语句。表达式 1 用作初始化,为循环变量赋初值。表达式 2 填写的是循环的条件。表达式 3 会更新循环变量的数值。

循环变量指的是什么?它其实相当于一个局部变量。我们先来看一个完整的写法。

	for(int i = 0; i < 10; i++){
		循环体
	}

for 语句要实现循环的功能,主要依赖一个局部变量,循环的终止会需要用到它。在上面例子中即是 i。表达式 1 完成了局部变量的初始化。之后循环每执行一遍,这个变量就必须更新。其中实现更新功能部分的就是表达式 3 中的 i++ ,透过它变量每次更新就会增加 1。到最后,循环体中的代码是不能无限循环下去的,否则后面的语句都无法执行,因此我们需要存在一个终止条件,表达式 2 就起到了这个作用。这里程序会判断 i 是否小于 10,当满足就继续执行,不满足就跳出循环。

因此,for循环中的语句执行顺序就是这样的。

    表达式1(局部变量初始化)
    表达式2(满足继续往下执行)
    循环体(第一次循环)
    表达式3 (更新)
    表达式2(满足继续往下执行)
    循环体(第二次循环)
    表达式3 (更新)
    表达式2(满足继续往下执行)
    循环体(第三次循环)
    ...
    ...
    表达式3 (更新)
    表达式2(不满足,跳出循环)

你可以对照这个执行顺序在脑海中模拟几遍。但不亲手敲一次代码是不可能真正理解的。当我们想摸清一个陌生的概念,可以先在控制台通过 println 语句输出数值。

--代码示例(5-1):

	void setup(){
		for(int i = 0; i < 10; i++){
			println ("run");
		}
	}

你可以数一数控制台中输出 run 的数量,这里刚好为 10 个。由此就获悉了循环体中的代码执行的次数。但这样做还是无法察觉循环内具体发生了哪些变化。我们可以把输出的字符 “run” 改成变量 i 试试。

--代码示例(5-2):

	void setup(){
		for(int i = 0; i < 10; i++){
			println (i);
		}
	}

现在能看到,在循环体中的 i 值是不断增大的。以后我们可以通过这个值,来了解当前循环的进度。

  • 在示例(5-2)中,i 的值是从 0 变化到 9。似乎和实际循环次数总是相差 1。若是觉得不习惯,for语句括号中的表达式也可以写成
     for(int i = 1; i <= 10; i++)

这样 i 就会恰好对应循环次数。 “<=” 含义是小于等于,所以当 i 等于 10 时会仍然满足条件,因此会比写成 i < 10 在末尾中可多循环一遍。尽管是从 1 开始,但循环的次数一样为 10。当然,若是没有特别必要的话还是建议采取例子开头的写法。后面介绍到的 vector 或数组,都是通过下标来获取元素的,而下标默认都是从 0 开始。初值先设为 0,是较为常用的做法。

  • 上例中若写 i 大于 0,程序是会崩溃的。因为变量持续递增,永远都会满足这个条件。这样相当于不能终止,程序会陷入死循环。

  • for语句中的局部变量不仅可以声明整形类型,还可以用诸如浮点类型来声明变量。可以写成 for(float i = 0;i < 10;i+=0.02)。

for循环解数学题

不了解大家是否还记得数学家高斯小时候的一则故事。高斯那时 10 岁,他的算术老师想在课堂上布置一个作业,题目是

1+2+3+4……+97+98+99+100=?

如果用手算,自然会耗费很长的时间。但高斯估计是自己琢磨出了等差数列的求和方法,所以在题目刚报出时,就轻松地报出答案,让老师大吃一惊。

现在我们可能不大记得等差数列求和究竟是什么了,但却可以用一种原始而暴力的方式得到答案。那就是 for 循环。反正计算对电脑而言,是小菜一碟。我们只要将问题描述为计算机可识别的语言,就能轻松获得答案。

--代码示例(5-3):

	void setup(){
	    int answer = 0;
	    for(int i = 1; i <= 100; i++){
	        answer += i;
	    }
	    println(answer);
	}

相信你得到的结果会和高斯报出的答案一样,为 5050!

  • Tips:for 循环中局部变量的名字可以随意更改,只要符合变量的命名规则即可。可以写成 (int k = 1;k <= 100;k++)。没有特殊情况,默认使用 i 作为变量名。

for循环绘图

经过一番略显枯燥的铺垫,我们终于可以进入更有意思的环节了。就是用 for 循环画图。那些枯燥的数值计算可以先搁到一边了,设计师还是对图形更为敏感~

利用 for 循环绘制一排圆

当我们想用 for 循环去表现一组重复的元素。只要事先确定这些元素间的数量关系,就能很方便地用 for 循环去实现,而无需做大量的重复劳动。假如要画一排水平方向均匀分布的圆,它的纵坐标是不变的。变化的只是横坐标,而这个横坐标从左到右,是不断递增的,并且递增距离都一样。这时就可以通过 for 循环中的 i,来确定每个圆的横坐标。

--代码示例(5-4):

	void setup(){
	    size(700, 700);
	    background(83, 51, 194);
	    noStroke();
	}
	void draw(){
	    for(int i = 0; i < 7; i++){
	       ellipse(50.0 + i * 100.0, height/2.0, 80.0, 80.0);        
	    }
	}

  • 50 代表左面第一个圆的起始位置,i * 100 中的 100 表示递增的距离。

利用 for 循环绘制随机圆点

上面图形的位置都是可预测的,这样就失去很多趣味。我们可以用前面提到的 random 函数,来创造更多的可能性。试试将它写在绘图函数中。

--代码示例(5-5):

	void setup(){
	    size(700, 700);
	    background(0);
	    noStroke();
	}
	
	void draw(){
		background(0);
      	for(int i = 0; i < 10; i++){
          float randomWidth = random(60.0);
          ellipse(random(width), random(height), randomWidth, randomWidth);
      }
	 }

这里的圆的位置之所以不断闪动,是因为 random 函数每执行一次,结果都是随机的。由于 draw 函数默认每秒运行 60 帧,所以每次绘制的 10 个圆在一秒内,会随机改变 60 次位置。这种快速的闪动就让画面看上去不止有 10 个圆了。

程序里改变一个简单的数值,就能获得截然不同的效果。我们可以通过修改循环终结条件来改变循环的次数。下图的循环终结条件为 i < 100

下面是循环终结条件为 i < 1000 时的效果

randomSeed

如果我不希望圆的位置是随机生成的,但又不希望它闪动。应该怎么做?一种做法是为每个圆的坐标都创建独立的变量去保存,并在 setup 中对这些变量进行初始化,一并赋上随机值。这样在 draw 里使用绘图函数,调用的就是变量中存储的数值,而不会随时发生改变。

画 10 个圆尚且可用这个方法去创建变量。但画 1000 个圆,10000 个圆。要创建这个数量的变量用传统的方式逐个去命名是会非常繁琐的。

我们先不用去学习新的创建变量的方式。这里有一个灵活的方法可以达到这个目的。就是使用 randomSeed。先看使用后的效果。

--代码示例(5-6):

	void setup(){
	    size(700, 700);
	    background(0);
	    noStroke();
	}
	
	void draw(){
		background(0);
		randomSeed(1);
      	for(int i = 0; i < 10; i++){
          float randomWidth = random(20.0, 60.0);
          ellipse(random(width), random(height), randomWidth, randomWidth);
      }
	 }

与前面的代码相比没有很大变化,除了让圆的半径随机范围改成从 20 到 60 之外,只多了一句 randomSeed。但用上这句之后,图形似乎都变静止了。

调用格式:

	randomSeed(a);

其中的 a 设置的是种子,需要填写整数(在 P5 中填写浮点数不会报错,但只会将它化成整数处理)。randomSeed 的作用是设定随机数的种子,它会根据不同的种子,生成不同的随机数序列。在它之后调用的 random 函数,返回的结果都是确定的。这里的确定不是指结果是一个定值,它确定的只是生成的序列。也就是说对应的调用次数,返回的结果是确定的。

--代码示例(5-7):

	void setup(){
		 randomSeed(0);
	    for(int i = 0; i < 5; i++){
	        println(random(10));
	    }
	}

继续用 println 做个实验。使用了 randomSeed 后,你每次关闭程序,再运行程序。返回的都会是一串同样的结果。数值与顺序会一一对应的。去掉的话,每次返回的都是不同的数值。

之所以有这种设置。是因为程序中的随机数,本身都是“伪随机”的。结果看似随机,实则都是通过一个固定的,可重复的计算方法产生的。randomSeed 就相当于指定某个原始值。之后的结果都会根据这个种子来推算。而当我们不指定种子时,程序会默认根据系统的当前时间来生成种子。因此每次运行结果都不一样。

下面的例子可以帮助你更好地理解 randomSeed。

--代码示例(5-8):

  void setup(){
      size(700, 700);
      background(0);
      noStroke();
  }

  void draw(){
      randomSeed(1);
      for(int i = 0; i < 10; i++){
          float randomWidth01 = random(10, 60);
          ellipse(random(width), random(height), randomWidth01, randomWidth01);
          println(randomWidth01);
      }
      
      randomSeed(1);
      for(int i = 0; i < 10; i++){
          float randomWidth02 = random(10, 60);
          ellipse(random(width), random(height), randomWidth02, randomWidth02);
          println(randomWidth02);
      }
  }

尝试将第二个 randomSeed(1) 修改成 randomSeed(0) 对比最终的结果。

  • Tips:P5 中只要在 draw 的结尾调用 noLoop 函数,就可以达到同样的效果,它的作用是令程序中止运行。与上面的实现原理有本质的区别。

for 循环画线

掌握 randomSeed 的用法之后,可以尝试替换绘图函数,例如将画圆变成画线。只要给直线的端点设计一些变化规则,就可用大量的线条交织出独特图案。

--代码示例(5-9):

  void setup(){
      size(700, 700);
      background(0);
  }

  void draw(){
      randomSeed(0);
      for(int i = 0; i < 2000; i++){
          float x1 = width/2.0;
          float x2 = random(50.0, 650.0);
          stroke(255, 20);
          line(x1, 50, x2, 650);
      }
  }

制造简单笔刷

再回到 for 循环。前面例举的示例都是没有交互的,要让结果更有趣可不能忘记将 mouseX,mouseY 结合到代码中。

--代码示例(5-10):

  void setup(){
      size(700, 700);
      background(255);
      noStroke();
  }

  void draw(){
      for(int i = 0; i < 1000; i++){
          fill(0, 30);
          float x = mouseX + random(-50, 50);
	       float y = mouseY + random(-50, 50);
          ellipse(x, y, 2, 2);
      }
  }

一个“散点”笔刷就诞生了。由于每个细密的圆点坐标都是基于鼠标位置,往左右上下四个方向随机移动有限的距离,所以笔刷最终的形状分布就会近似于方形。

--代码示例(5-11):

	void setup(){
      size(700, 700);
      background(255);
      noStroke();
   }
  
  	void draw(){
      for(int i = 0; i < 1000; i++){
          float ratio = mouseX/(float)width;
          float x = mouseX + random(-50, 50);
	       float y = mouseY + random(-50, 50);
	       fill(0, ratio * 255, 255 * (1 - ratio), 30);  
          ellipse(x, y, 2, 2);
      }
   }

若是用 mouseX 的值来影响填充颜色,会得到更迷幻的色彩渐变

for 循环的嵌套

for 循环是可以嵌套的。可以在 for 循环里面再写一个 for 循环。当你需要绘制二维的矩阵就可以采取这种写法。

--代码示例(5-12):

	void setup(){
      size(700, 700);
      background(202, 240, 107);
   }

	void draw(){
      fill(0);
      for(int i = 0;i < 5;i++){
          for(int j = 0;j < 5;j++){
          	float x = 150 + i * 100;
          	float y = 150 + j * 100;
              ellipse(x, y , 60, 60);
              println(i + ":" + j);
          }
      }
   }

初次使用嵌套循环,需要理清其中的逻辑关系。程序中的代码执行顺序始终是由上至下的,因此首先执行的必定是最外层的循环。外循环每执行一次,内循环就要持续执行直到不满足条件为止,此后才执行第二次的外循环。第二次开始后,内循环又会继续从头执行直到条件不满足,如此反复,直到所有条件都不满足,跳出循环为止。

上面的代码,外循环中的循环体一共执行了 5 次,而内循环中的循环体则执行了 25 次。这 25 次中,根据 i ,j 的数值不同,分别用来确定圆的横纵坐标。例子中嵌入了一段 print ,你可以观察输出的数值揣摩其中的变化。仅仅用两个循环嵌套,就能将 i,j 的数值组合都遍历了。

  • Tips
  • 第二层的 for 循环一般在开头键入 Tab 进行缩进,这样做可以使代码结构更清晰
  • 两层 for 循环中的局部变量必须起不同的变量名。其中,”i”,“j”,“k” 是最为常用的

灵活运用 “i” ,”j”

“i”,“j” 这两个变量名指代的是两层 for 循环中的局部变量。下面的例子会加深你对 “i””j” 的理解。根据“i” ,“j” 值的不同,可以传入参数来对元素进行“分组”。

--代码示例(5-13):

void setup() {
  size(700, 700);
  background(0);
  noStroke();
}

void draw() {
  background(0);
  fill(250, 233, 77);  

  for (int i = 0; i < 7; i++) {
    for (int j = 0; j < 7; j++) {
      pushMatrix();
      translate(50 + i * 100, 50 + j * 100);

      // 设置 1
      //float angle = sin(millis() / 1000.0) * PI/2;

      // 设置 2
      //float ratio = i/7.0;
      //float angle = sin(millis() / 1000.0 + ratio * (PI/2)) * PI/2;

      // 设置 3
      float ratio = (i * 7 + j)/49.0;
      float angle = sin(millis() / 1000.0 +  ratio * (PI/2)) * PI/2;

      rotate(angle);
      rectMode(CENTER);

      // 绘制图形 1
      rect(0, 0, 80, 80);

      // 绘制图形 2
      // rect(0, 0, 100, 20);

      // 绘制图形 3
      //rect(0, 0, ratio * 50, ratio * 50);

      popMatrix();
    }
  }
}

代码说明

  • rectMode(CENTER) 可以改变矩形的绘制方式,原来 rect 的前两个参数是用来确定矩形左上角的坐标。开启此命令后,这两个参数会用于设定矩形中心的坐标。由于这里是通过 rotate 来对图形进行旋转操作的,所以就需要通过这种方式,将矩形的中心绘制到坐标原点上。
  • millis() 获取的是程序从运行到当前所经过的时间,单位是毫秒。此值会影响 sin 输出值的变化速度,若直接写 millis ,变化幅度则太大,因此将它除以 1000.0

这段代码中运用注释符 “//” 隐藏了多个设置。你可以通过开启或是关闭去快速地切换效果。例如开启了 “设置 3” 后的语句,就需要把 “设置 1” 和 “设置 2” 后的代码块用注释符关闭掉。对于这类程序结构相似而局部代码有所区别的例子,就可以用这种形式去写,这样就无需分别保存多个工程文件了。练习和创作的时候可以多运用这种技巧,以此来保存一些自己满意的参数设置。

其中 i,j 值对程序的影响,主要通过切换 “设置1(设置2)(设置3)” 来体现。可以对比下面的输出结果

绘制图形 1:设置 1

绘制图形 1:设置 2

绘制图形 1:设置 3

绘制图形 2:设置 1

绘制图形 2:设置 2

绘制图形 2:设置 3

在设置 1 中,没有使用到 i 或 j 去影响每个元素的旋转角度。因此可以看到每个元素的运动效果都是一致的。而设置 2 用到了 i 值,设置 3 同时用到了 i 和 j。它们最终通过 ratio 值去影响 sin 函数的参数输入,以此改变 angle 的周期变化。由于设置 2 与设置 3 的具体效果在动图中并不明显,我们可以通过下面的截图去观察。

绘制图形2( 左图:设置2 - 右图:设置3 )

绘制图形3( 左图:设置2 - 右图:设置3 )

第一张图中,ratio 用于影响矩形的旋转角度。而第二张图,则直接用来控制圆形的半径大小。可以看到,只使用了 i 值的语句:

float ratio = i/7.0;

它的纵向元素的变化都是完全一致,因为控制图形横坐标的只依赖 i 值,所以横坐标相同的图形,ratio 的值也相同,旋转角度,圆的半径大小也相同。

而同时用到 i,j 的语句

float ratio = (i * 7 + j)/49.0;

它可以描述“渐变”,这里通过相乘一个系数的方式,将行与列的影响组合到了一起。使每个元素都有所区别。

While 循环

for 循环还有一个兄弟。那就是 while 循环。for 循环能做的事,while 循环也能做。只是 while 循环的在 creativeCoding 中的使用频率并没有 for 循环高。

--代码示例(5-14):

	void setup(){
	    int a = 0;
	    while(a < 10){
	        println(a);
	        a++;
	    }
	}

while 的语法结构其实比 for 更好理解。while 语句的前面先创建变量,接着中括号内填写一个表达式,当满足时就执行循环体中的语句,最后在循环体内放上一个对变量进行更新的表达式,这样 while 循环就完成了。对于循环次数确定的,多用 for 循环。当变量的数值不确定时,更推荐使用 while 循环。

思考题

  • 1.尝试用各种基本型替换 for 循环中的绘图元素,制造各种与别不同的笔刷
  • 2.结合上章提到的三角函数,尝试将方形的“散点”笔刷修改成圆形
  • 3.尝试只用一个 for 循环绘制出二维矩阵

下节预告

跟随教程会发现。每新学一个知识点,可玩性就瞬间倍增,因为多了更多的组合可能性。程序是个潘多拉魔盒,你能想到的,它都能帮你做到,所以没有理由不去学习这门能与计算机沟通的语言。

下节将会介绍另一种流程控制语句 - if,它可以控制程序流,产生更复杂多变的结果。通过 if 语句,你可以轻松地打造属于自己的文字冒险游戏!