如何适应所有的地图?
我们还是继续完成上一期遗留的任务:如何设计适应所有地图的程序?如果我们自己是Karel机器人的时候,我们如何去拥抱这个未知的世界?我们是不是一直往前走,只要前面有路,我们就不断往前,那什么时候停止了?直到我们遇到了墙为止。那我们程序如何对应实现这个思想了?我们回忆一下讲循环的时候,是不是提到循环,有两种方式,一种是for 循环,for语句用于当你想按预定的次数重复执行一组命令的时候,while语句用于当你想在某些条件满足时候,重复执行一组命令的时候。好像while可以解决我们的问题。我们看看while的语法
while(条件){
重复执行的语句。
}
当条件被满足的时候,就一直执行大括号里面的命令。
我们依然将任务先分解一下,先完成放满一行的程序,如何完成放满一行的程序了?while里的条件是什么?
当前面道路是通的话,Karel继续往前走,那么while里的条件是前面道路是不是通的?如果条件是通的,就一直执行循环体里的内容。那如何检测前面道路是不是通的,系统提供了一系列检测条件:frontIsClear()检测前面道路是不是通的,如果是通的就表示条件为真,不通表示条件为假。在while循环里,使用frontIsClear()作为检测条件,karel将重复执行循环命令,直到它前面遇到墙,循环结束。这样就可以适用所有地图。
我们还是先完成填满一行的命令:
public void createBeeperLine(){
while(frontIsClear()){
putBeeper();
move();
}
}
大家觉得是不是能完成我们的目标,我们现在已经开始有意识到off by one的错误。当前面有墙的时候,Karel就不执行循环体里的内容,不往前走,但是也不要忘了在当前位置放下方块。那我们很容易解决这个问题:
public void createBeeperLine(){
while(frontIsClear()){
putBeeper();
move();
}
putBeeper();
}
这样填满一行的问题解决了,那么我们接下来要完成所有行如何去做?
跟放满一行类似,我们不知道具体的行数,我们使用while函数,while里的判断条件依然是前面是不是道路是不是通的?如何判断是不是前面道路是不是通的?用frontIsClear()命令可不可以?这个时候我们要注意Karel的朝向,必须把它调整到面朝北向,也就是朝上,我们才能使用frontIsClear()判断上面是不是还有新的一行。
系统有没有提供像frontIsClear()这样的命令直接判断上面是不是还有一行了?很不幸,没有,宝宝不开心啦….我们能不能自己写一个这样的命令,现在系统提供了如下的命令:
leftIsClear() | leftIsBlocked() | 左边是不是有墙? |
rightIsClear() | rightIsBlocked() | 右边是不是有墙 |
我们结合上一次的命令:
facingEast() | notFacingEast() | Karel是不是面朝东方的 |
facingWest(): | notFacingWest(): | Karel是不是面朝西方的 |
facingNorth() | notFacingNorth() | Karel是不是面朝北方的 |
facingSouth() | notFacingSouth() | Karel是不是面朝南方的 |
我们是不是自己能封装一个northIsClear和northIsBlocked的命令了:
我们看看如果karel现在面朝向东,而它的左手边是通的话,那么它的上边就是通的。如果Karel现在朝西的话,而它的右手边是通的话,那么它的上边就是通的,否则就是不通的。大家一定要搞清楚东西南北的方向,千万不要是一个路痴。我们如何去实现这样的一个命令,我们之前只实现过像右转,或者放一行的命令。我们并没有实现过这种带着开放性的命令,判断一件事是不是正确。我们如何完成这种像问答式的命令了。
我们这个时候再回忆我们再写方法的时候的格式:
public void run(){
方法体;
}
我们当时说方法体的头两个单词public void是Java的格式,我们先忽略它们。现在我们看看第二个单词的作用:方法的返回值。java中方法,其实就是执行一个动作的。比如“调用XX方法计算年收入”、 “调用XX方法打印出学生学号”,这些方法都是有一个执行目的,每一个方法都是为一个执行目的而生的,返回值就与那个方法的目的有关。
比如我们调用XX方法计算年收入,我们的目的是得到全年的收入,那我们需要那个方法完成两件事:一是计算年收入并且返回收入值给我们,收入值就是这个方法的返回值,这个返回值的数据类型就是方法的返回类型,比如这里,我们定义为double(小数型)。
而有的方法我们不需要他返回什么值,比如我们之前调用向左转命令turnLeft(),我们不需要得到任何反馈,只需要他做就行了,于是就定义这个方法的返回类型为void,意思是没有返回值。
所以,其实方法的返回类型就是他返回的那个数据的类型,如果不返回任何数据,就是void!我们之前使用的方法使用void就意味着不需要返回任何数据,而仅仅需要做那件事。
那么现在我们需要返回一个逻辑判断的数据类型,返回类型是boolean型的。只能是true(真)或false(假)两个值之一。那么northIsClear()命令的格式如下:
public boolean northIsClear(){
if(条件){
return true;
}
else{
return false;
}
}
现在还有一个问题是我们在上面分析的条件:Karel现在面朝向东,而它的左手边是通的话。或者朝西的话,而它的右手边是通的话。那么Karel的北向是通的,我们如何用代码去表达这样复杂的逻辑运算。我们简单的看一下逻辑运算规则:
有几个组合布尔型数值的运算符,包括:布尔与(AND),布尔或(OR)和布尔非(它们分别对应&&、||、!),以及产生boolean型结果的比较运算符。
逻辑运算符(Logical Operators)
操作数的逻辑关系,计算结果“true”或“false”
逻辑与 && “op1 && op2”
1) 操作数都为真“true”,结果为真“true”
2) 否则结果为假“false”
逻辑或 || “op1 || op2”
1) 有一个操作数为真“true”,结果为真“true”
2) 否则结果为假“false”
逻辑非 ! “! op”
1) 取反,操作数为真“true”,结果为真“false”,反之……
比如我们这里Karel满足面朝向东,而它的左手边是通的话,这个时候北向是通的条件就为真,用代码去表示就是:
if(facingEast()&& leftIsClear()){
return true;
}
现在我们可以完成northIsClear()的代码如下:
public boolean northIsClear(){
if (facingEast() && leftIsClear()){
return true;
}
if (facingWest() && rightIsClear()){
return true;
}
return false;
}
public boolean northIsBlocked(){
if (facingEast() && leftIsClear()){
return false;
}
if (facingWest() && rightIsClear()){
return false;
}
return true;
}
我们再从整体上思考一下我们如何完成任务:我们是不是每次先放满一行,然后判断上面有没有通路,有的话,就往上走一行,继续执行放满一行的命令。如何没有的话,就结束任务。对应的完整代码如下:
public class CollectNewspaperKarel extends Karel {
// You fill in this part
public void run(){
boolean hasNotCompleted= true;
while(hasNotCompleted){
createBeeperLine();
if (northIsBlocked()){
hasNotCompleted = false;
}
if(hasNotCompleted){
moveToNextRow();
}
}
}
public void createBeeperLine(){
while(frontIsClear()){
putBeeper();
move();
}
putBeeper();
}
public void moveToNextRow(){
if (facingEast()){
turnLeft ();
move();
turnLeft ();
}
else{
turnRight();
move();
turnRight();
}
}
public boolean northIsClear(){
if (facingEast() && leftIsClear()){
return true;
}
if (facingWest() && rightIsClear()){
return true;
}
return false;
}
public boolean northIsBlocked(){
if (facingEast() && leftIsClear()){
return false;
}
if (facingWest() && rightIsClear()){
return false;
}
return true;
}
}
我们测试代码,是不是发现能够顺利完成任务。我们在这个任务的基础上完成新的一点挑战:在空的矩形完成跳棋盘,如下图
我们发现只需要改写createBeeperLine()就可以完成目标了,如何改写?我们仔细观察,可以发现就是隔一个放一个,我们如何用代码去实现了,我们同样可以分两种情况:
1) 如果当前位置有Beeper的话,接下来执行的步骤是什么?
2) 如果当前位置没有Beeper的话,接下来做什么?
我们仔细想一想很快得出结论:如果当前位置有Beeper的话,接下来移到下一个位置,下一个位置不放Beeper。
如果当前位置没有Beeper的话,那么移到下一个位置,必须放Beeper。
同时,我们需要记住每一行的起始位置放不放Beeper,需要事先知道。我们定义theFirstPutBeeper变量表示这一行第一个究竟放不放Beeper。
函数就行了:
public void createBeeperLine(){
if (theFirstPutBeeper){
putBeeper();
}
while(frontIsClear()){
if(beepersPresent()){
move();
}
else
{
move();
putBeeper();
}
}
}
我们要考虑处理theFirstPutBeeper变量,在哪一行第一个需要放,哪一行不需要放,我们怎么处理了?
其实我们在一行结束的时候,需要移动到上一行的时候,可以处理。如果当前行结束的位置有方块的话,那么下一行的开始的位置就不用放Beeper,也就是theFirstPutBeeper = false;如果当前行结束的位置没有Beeper,那么下一行开始的时候就需要放Beeper,也就是theFirstPutBeeper = true;那么我们现在需要一个判断当前位置是否有Beeper的命令。幸运的是系统提供了这个命令:
beepersPresent( ):当前位置是否有Beepers。
完整的代码如下:
public class CheckerboardKarel extends SuperKarel {
// You fill in this part
boolean theFirstPutBeeper = true;
public void run(){
boolean hasNotCompleted= true;
while(hasNotCompleted){
createBeeperLine();
if (northIsBlocked()){
hasNotCompleted = false;
}
if(hasNotCompleted){
if(beepersPresent()){
theFirstPutBeeper = false;
}
else{
theFirstPutBeeper = true;
}
moveToNextRow();
}
}
}
public void createBeeperLine(){
if (theFirstPutBeeper){
putBeeper();
}
while(frontIsClear()){
if(beepersPresent()){
move();
}
else{
move();
putBeeper();
}
}
}
public void moveToNextRow(){
if (facingEast()){
turnLeft ();
move();
turnLeft ();
}
else
{
turnRight();
move();
turnRight();
}
}
public boolean northIsClear(){
if (facingEast() && leftIsClear()){
return true;
}
if (facingWest() && rightIsClear()){
return true;
}
return false;
}
public boolean northIsBlocked(){
if (facingEast() && leftIsClear()){
return false;
}
if (facingWest() && rightIsClear()){
return false;
}
return true;
}
}
我们发现相比较上一个任务,我们就只稍微改动了一点点就完成了这个任务。我们是不是体会到了,好的代码设计,非常容易扩展,适应变化的需求。当需求发现变化的时候,我们能够很快地扩展了原来的应用。 我们发现两者的代码非常接近,除了createBeeperLine()稍微不一样,其他基本一致,我们有没有办法,只用一套代码实现两种任务。这个任务稍微有点挑战,等以后大家学习了Java的三大特性之一的多态,就很容易处理。等那个时候,我们再来解决这个问题,体现面向对象的强大。
在完成整个代码的过程中,我们不仅对循环,判断语法变得熟悉起来,也体会到将任务不断分解,不仅让逻辑变得清晰,结构合理,容易扩展,而且出错的几率也相对少。我们下一节继续探讨任务的分解,欢迎收看下一期课程:Karel的化整为零大法。