第 3 章 条件语句
本章介绍如何定义代码执行规则。这样的语法规则叫作控制流,允许开发人员编写各种条件语句,控制应用程序代码段该在何时运行。你将首先学习if/else
语句和表达式以及when
表达式,然后学习使用比较运算符和逻辑运算符编写true/false测试,最后学习Kotlin的string模板功能。
为了学习如何运用这些概念,我们会创建一个叫作NyetHack的项目。这是个重要项目,后续大部分章节都会用到它。
NyetHack?为什么取这个名字?问得好。还记得1987年发布的《迷宫黑客》(NetHack)游戏吗?这是个单人操作、带ASCII图像的文字冒险游戏,由NetHack DevTeam公司开发。NyetHack就是类似于NetHack的文字游戏(不好意思,不带ASCII图像)。Kotlin语言的缔造者JetBrains公司在俄罗斯设有办公室。这样一款类似于NetHack的文字游戏,再加上Kotlin的俄罗斯起源,就是NyetHack背后的故事。
3.1 if/else
语句
让我们开始吧!启动IntelliJ并创建一个新项目。(IntelliJ开着的话,直接点选File → New → Project...菜单项。)项目基于Kotlin/JVM,在项目名处输入NyetHack。
在项目工具窗口中,展开NyetHack项目,右击src文件夹新建一个名为Game的Kotlin File/Class文件。在Game.kt文件中,输入main
并按Tab键添加main入口函数。完成后的代码如下所示。
fun main(args: Array<String>) { }
在NyetHack项目中,玩家的健康状况由当前健康值决定,取值范围是0~100。游戏征途中,玩家可能会在战斗中受伤,也可能没遭遇什么,因而健康值爆表。对于玩家的可视健康状况该如何描述,你需要制定规则:如果健康值是100,就说明玩家健康状况极佳,否则就告诉玩家他的受伤程度。很简单,使用if/else
语句就能实现这样的规则。
在main
函数中,参照代码清单3-1,编写出你的首个if/else
语句。稍后会详解这些代码干了些什么。
代码清单3-1 显示玩家的健康状况(Game.kt)
fun main(args: Array<String>) { val name = "Madrigal" var healthPoints = 100 if (healthPoints == 100) { println(name + " is in excellent condition!") } else { println(name + " is in awful condition!") } }
我们来逐行分析一下新代码。首先,定义一个name
只读变量,赋给它一个字符串值作为勇敢玩家的名字。接着,定义healthPoints
变量并赋初始值100。最后,添加一条if/else
语句。
在if/else
语句内,提出这样的true/false问题:“玩家的healthPoints
分值是100吗?”这里用到了==
运算符,读作“等于”,所以这条问句就读作“如果healthPoints
等于100”。
if
语句后面跟着一条语句(置于花括号{}
中)。这条就是你想让应用程序执行的语句,但条件是if
表达式的布尔结果值为true
,也就是说,healthPoints
变量值正好是100。
if (healthPoints == 100) { println(name + " is in excellent condition!") }
函数println
你已熟悉,它用来在控制台输出信息。这里输出的是name
变量值和字符串" is in excellent condition!"
(注意前面的空格,有了它,才不至于出现Madrigalis in excellent condition!
这样的结果)。目前为止,我们的if/else
语句就是说:如果Madrigal的健康值是100,程序就应该在控制台输出英雄健康状况极佳的信息。
(本例中,if
语句后面的花括号里只有一条语句,但是如果满足if
语句的true值条件,有多项任务要执行,可以添加多条语句。)
+
运算符可以把一个值和一个字符串拼在一起,这种行为叫字符串拼接。这样,基于变量值,我们就能轻松定制输出到控制台的信息。本章后面,你还会看到另一种更好的向字符串注值的方式。
如果healthPoints
健康值不是100会怎样?果真如此的话,if
表达式的求值结果就是false,编译器会忽略花括号中紧接if
的语句,直接跳到else
部分。else
就是“否则,不然”的意思:if
满足条件,做这些;否则,就做那些。像if
一样,else
后面也跟着一个或多个表达式,这些表达式放在花括号里,告诉编译器该做什么。和if
不同,else
不需要定义条件。只要不满足if
中的条件,都走else
分支,所以它后面直接跟着一对花括号。
else { println(name + " is in awful condition!") }
else
分支中的println
函数和if
分支中的不同之处就是,英雄name
后面跟着的字符串不一样,一个是“is in excellent condition! ”,另一个是“is in awful condition! ”。(目前为止,我们接触的只有println
函数。关于函数的更多知识,包括如何自定义函数,详见第4章。)
好了,可以用通俗语整体描述了。上述if/else
代码就是告诉编译器,如果英雄的健康值正好是100,就在控制台输出Madrigal is in excellent condition!
(Madrigal健康状况极佳!),否则就输出Madrigal is in awful condition!
(Madrigal健康状况不妙!)。
运算符==
只是Kotlin众多比较运算符中的一种。表3-1列出了Kotlin支持的其他各种比较运算符,现在大致了解就可以了,后面还会具体学习。在需要使用某些运算符来表达条件的时候,你可以回头来参考这张表。
表3-1 比较运算符
运算符 |
描述 |
---|---|
|
计算左侧值是否小于右侧值 |
|
计算左侧值是否小于等于右侧值 |
|
计算左侧值是否大于右侧值 |
|
计算左侧值是否大于等于右侧值 |
|
计算左侧值是否等于右侧值 |
|
计算左侧值是否不等于右侧值 |
|
计算两个实例是否指向同一引用 |
|
计算两个实例是否不指向同一引用 |
可以运行应用了。单击main
函数左侧的运行按钮运行Game.kt,应该能看到以下输出:
Madrigal is in excellent condition!
既然healthPoints == 100
条件表达式的计算值是true,if/else
语句中的if
分支就被触发了(我们使用分支的说法,是因为取决于指定条件是否满足,代码执行流会分流)。现在,如代码清单3-2所示,把healthPoints
变量值改为89。
代码清单3-2 修改
healthPoints
变量值(Game.kt)
fun main(args: Array<String>) { val name = "Madrigal"var healthPoints = 100var healthPoints = 89 if (healthPoints == 100) { println(name + " is in excellent condition!") } else { println(name + " is in awful condition!") } }
再次运行应用。你会看到如下输出:
Madrigal is in awful condition!
现在,if
条件表达式的计算结果是false(89不等于100),所以else
分支被触发了。
3.1.1 添加更多条件
玩家的健康状况值粗略地反映了玩家的健康状况。注意“粗略“这个词。如果healthPoints
变量值是89,程序会告诉你,玩家的健康状况有点糟。显然,这结论不靠谱,毕竟89很可能只是代表受了皮肉伤。
为了使if/else
语句的表达更精准,获得更多可能的结果,你可以添加更多条件和分支。else if
分支的作用就在于此,它的语法类似于if
,用在if
和else
之间。如代码清单3-3所示,更新if/else
语句,添加3个else if
分支,以检查healthPoints
变量的中间值。
代码清单3-3 掌握玩家的更多健康状况(Game.kt)
fun main(args: Array<String>) { val name = "Madrigal" var healthPoints = 89 if (healthPoints == 100) { println(name + " is in excellent condition!") } else if (healthPoints >= 90) { println(name + " has a few scratches.") } else if (healthPoints >= 75) { println(name + " has some minor wounds.") } else if (healthPoints >= 15) { println(name + " looks pretty hurt.") } else { println(name + " is in awful condition!") } }
新的代码逻辑解读如下表所示。
Madrigal健康分值 |
应输出信息 |
---|---|
100 |
Madrigal is in excellent condition!(极为健康) |
90~99 |
Madrigal has a few scratches.(小擦伤) |
75~89 |
Madrigal has some minor wounds.(小伤口) |
15~74 |
Madrigal looks pretty hurt.(受伤严重) |
0~14 |
Madrigal is in awful condition!(情况不妙) |
再次运行应用。因为Madrigal的healthPoints
值是89,所以if
分支和第一个else if
分支表达式的结果都为false。但else if (healthPoints >= 75)
的结果为true,所以控制台输出的是“Madrigal has some minor wounds.
”。
编译器计算if/else
条件表达式的顺序是自上而下,并且一旦得到true值就停止。如果所有条件都不满足,就执行else
分支。
由此可见,条件表达式的顺序至关重要。如果你按从低到高的顺序安排if
和else if
分支,那所有的else if
分支都没机会执行。任何大于等于15的healthPoints
值都会触发第一个分支,而任何小于15的healthPoints
值只会走else
分支。(以下代码仅作讲解用,请勿修改项目实际代码。)
fun main(args: Array<String>) { val name = "Madrigal" var healthPoints = 89 if (healthPoints >= 15) { // Triggered for any value of 15 or higher println(name + " looks pretty hurt.") } else if (healthPoints >= 75) { println(name + " has some minor wounds.") } else if (healthPoints >= 90) { println(name + " has a few scratches.") } else if (healthPoints == 100) { println(name + " is in excellent condition!") } else { // Triggered for values 0-14 println(name + " is in awful condition!") } }
添加了更多的else if
分支语句后,玩家的健康状况报告更精细准确了。作为练习,试试修改healthPoints
值,触发一下所有新加分支。完成后,将healthPoints
值再改成89。
3.1.2 if/else
嵌套语句
在NyetHack游戏里,玩家可能很走运。例如,身体基本素质高的人,如果受了点小伤,能很快恢复。接下来,你要添加新的变量来处理这种情况(想想要用哪种数据类型),如果真的很走运,添加相应的健康状况报告文字以反映实际情况。
为了完成这个任务,需要在某个分支里嵌套一个if/else
语句,实现在玩家的healthPoints
值大于等于75时,检查嵌套进来的if/else
语句,看看玩家是否走运。(如代码清单3-4所示,添加新代码时,不要漏掉最后一个else if
之前的花括号。)
代码清单3-4 看看走不走运(Game.kt)
fun main(args: Array<String>) { val name = "Madrigal" var healthPoints = 89 val isBlessed = true if (healthPoints == 100) { println(name + "is in excellent condition!") } else if (healthPoints >= 90) { println(name + " has a few scratches.") } else if (healthPoints >= 75) { if (isBlessed) { println(name + " has some minor wounds but is healing quite quickly!") } else { println(name + " has some minor wounds.") } } else if (healthPoints >= 15) { println(name + " looks pretty hurt.") } else { println(name + " is in awful condition!") } }
在上述代码中,你新加了一个布尔型可变变量,代表玩家是否走运。此外,还插入了一个if/else
语句,当玩家健康值在75至89之间时,输出新的健康状况信息。运行应用程序,看看是否得到了以下新输出。
Madrigal has some minor wounds but is healing quite quickly!(Madrigal受了点伤,但恢复极快!)
如果看到不一样的结果,请对照代码清单3-4,仔细检查代码,尤其是看看healthPoints
变量值是不是89。
通过嵌套条件表达式,你可以在分支里创建逻辑分支,实现更精准、更复杂的条件判断。
3.1.3 更优雅的条件语句
使用条件语句时,如果过于随意,很容易因不断地无脑添加而泛滥成灾。感谢Kotlin的精心设计,让我们既能够享受到条件语句的优点,又能编写出简洁易读的代码来。下面来看几个例子。
逻辑运算符
在NyetHack游戏里,会出现越来越复杂的条件状态需要你判断。例如,如果玩家比较走运并且健康值大于50,或者他们是永生之人,那么他们头上就会出现光环。否则,玩家光环裸眼是看不到的。
当然,你可以用一系列的
if/else
语句来判断玩家是否有可见光环,但后果是代码里充斥着重复代码,逻辑条件难以理清。别怕,我们有更优雅易读的方式:在条件语句里使用逻辑运算符。如代码清单3-5所示,新增一个变量和一条
if/else
语句,在控制台打印出光环信息。代码清单3-5 在条件语句里使用逻辑运算符(Game.kt)
fun main(args: Array<String>) { val name = "Madrigal" var healthPoints = 89 val isBlessed = true val isImmortal = false // Aura if (isBlessed && healthPoints > 50 || isImmortal) { println("GREEN") } else { println("NONE") } if (healthPoints == 100) { ... } }
新添加的
isImmortal
只读变量用来记录玩家是否永生(是否永生不能改,所以只读)。定义变量你已熟悉,下面来看几样新东西。首先是以
//
标注的代码注释。代码中,//
之后的任何当前行文字都是注释,编译器会直接忽视,所以如果你想写点什么内容,是不受Kotlin语法限制的。注释可以组织代码、说明代码用途,方便他人阅读(也方便自己将来回忆代码细节)。接下来是
if
表达式中的两个逻辑运算符。逻辑运算符和比较运算符组合起来,可以写出更长的表达式语句。&&
是逻辑与运算符,它需要&&
左右两边的条件语句求值结果都为true,才能得出整体true值。||
是逻辑或运算符,要得出整体true值,需要左右两边任意一边条件语句的求值结果为true,或者两边条件语句的求值结果都为true。表3-2列出了Kotlin的逻辑运算符。
表3-2 逻辑运算符
运算符 说明 &&
逻辑与:当且仅当两者都为true时,结果才为true(否则为false) \|\|
逻辑或:任意一个为true时即为true(只有两者都为false时结果才是false) !
逻辑非:true变false,false变true 小提示:运算符组合使用时,求值顺序要由优先级决定。优先级相同则遵循从左至右的原则。也可以把多个运算符放在括号里,作为一个整体参与运算。从高到低,以下是它们的优先级顺序:
!
(逻辑非)<
(小于)、<=
(小于等于)、>
(大于)、>=
(大于等于)==
(全等于)、!=
(不等于)&&
(逻辑与)||
(逻辑或)回到NyetHack项目上来,我们来看以下新增条件语句:
if (isBlessed && healthPoints > 50 || isImmortal) { println("GREEN") }
如果玩家运气好且健康值大于50,或者玩家获得永生,那么绿色光环应可见。Madrigal不能永生,但运气好且健康值是89。所以,第一个分支满足条件,Madrigal头上应出现光环。运行应用程序,看看是不是这样。你应该能看到以下控制台输出:
GREEN Madrigal has some minor wounds but is healing quite quickly!
上述逻辑使用嵌套条件语句也能实现,但要想清晰地表达复杂逻辑,还是要用逻辑运算符。
光环判断代码比
if/else
嵌套语句条理清晰多了,但代码还能写得更加简洁易读。除了条件语句,逻辑运算符还能用于许多其他表达式,包括变量定义。如代码清单3-6所示,添加一个布尔变量来封装光环判断条件,然后重构(重写)条件语句来使用这个新变量。代码清单3-6 在变量定义时使用逻辑运算符(Game.kt)
fun main(args: Array<String>) { ... // Aura
if (isBlessed && healthPoints > 50 || isImmortal) {val auraVisible = isBlessed && healthPoints > 50 || isImmortal if (auraVisible) { println("GREEN") } else { println("NONE") } ... }上述代码中,光环判断语句移到了
auraVisible
只读变量定义里,if/else
语句只需判断auraVisible
变量值即可。这和前面的代码功能等效,只不过改用变量表达式求值并赋值了。变量后面的表达式定义的规则很好读;定义了什么,看变量名便一目了然。应用程序的逻辑越来越复杂时,这种方式非常有用,它也有助于将来的代码阅读者理解你的表达意图。再次运行应用程序,确认代码功能和以前一样,控制台输出结果也相同。
条件表达式
现在,
if/else
语句正确输出了玩家的健康状况,内容也细致了许多。另一方面,因为每个分支都重复着类似的
println
语句,所以添改代码就显得有点烦琐。想一想,万一你要大改玩家状况的报告格式,该怎么办?基于当前的应用程序代码,你需要查看if/else
语句的每个分支,修改每个println
函数,用上新格式。修改
if/else
语句,改用条件表达式可以解决上述问题。条件表达式类似于条件语句,不同点在于,你把if/else
语句赋值给了后面会用到的某个变量。参照代码清单3-7,完成代码修改。代码清单3-7 使用条件表达式(Game.kt)
fun main(args: Array<String>) { ...
if (healthPoints == 100) {val healthStatus = if (healthPoints == 100) {println(name + "is in excellent condition!")"is in excellent condition!" } else if (healthPoints >= 90) {println(name + " has a few scratches.")"has a few scratches." } else if (healthPoints >= 75) { if (isBlessed) {println(name + " has some minor wounds but is healing quite quickly!")"has some minor wounds but is healing quite quickly!" } else {println(name + " has some minor wounds.")"has some minor wounds." } } else if (healthPoints >= 15) {println(name + " looks pretty hurt.")"looks pretty hurt." } else {println(name + " is in awful condition!")"is in awful condition!" } // Player status println(name + " " + healthStatus) }(顺便提一句,修改代码时,被代码缩进搞烦了的话,可以找IntelliJ帮忙。选择Code → Auto-Indent Lines菜单项,清爽的代码唾手可得也。)
根据
healthPoints
值,对if/else
表达式求值,"is in excellent condition!"
等语句值就赋给了healthStatus
变量。这就是条件表达式好用的地方。为了打印玩家的健康状况,现在用的是healthStatus
变量值,所以,可以删除6个差不多一样的输出语句。需要基于某个条件给变量赋值时,都可能用得上条件表达式。不过要记住,通常只有在各分支的返回值都是同一类型时(类似于
healthStatus String
变量的例子),条件表达式才最直观。使用条件表达式,光环判断代码还能更简洁高效。请动手实现。
代码清单3-8 使用条件表达式优化光环判断代码(Game.kt)
... // Aura val auraVisible = isBlessed && healthPoints > 50 || isImmortal
if (auraVisible) {println("GREEN")} else {println("NONE")} val auraColor = if (auraVisible) "GREEN" else "NONE" println(auraColor) ...再次运行应用程序,确保代码运行如常。你应该看到同样的输出结果,但代码更优雅易读了。
你可能已注意到了,光环判断条件表达式的两对花括号不见了。我们一起来看看何以如此。
删除
if/else
表达式的括号只有单个匹配答案满足条件时,省略包裹表达式的花括号才是有效的(至少语义上有效,稍后详谈)。在一个分支只包含一条语句的情况下,花括号才能省略,否则,代码的执行会受影响。
请看以下不带花括号版的
healthStatus
变量的赋值:val healthStatus = if (healthPoints == 100) "is in excellent condition!" else if (healthPoints >= 90) "has a few scratches." else if (healthPoints >= 75) if (isBlessed) "has some minor wounds but is healing quite quickly!" else "has some minor wounds." else if (healthPoints >= 15) "looks pretty hurt." else "is in awful condition!"
这个版本的代码和NyetHack应用中带花括号的版本做的是同样的事。表达同样的逻辑时,这个版本的代码量少了些,但一瞥之下,哪个版本更易读好懂,你自有判断。如果你选择带花括号的版本,那我告诉你,Kotlin社区也偏爱这种。
条件语句或表达式跨越多行时,建议你不要丢到花括号。原因有二。首先,如果没有花括号,那么在条件不断添加时,各个分支从哪里开始、在哪里结束会越来越难理清。其次,如果没有花括号,那么新加入的代码贡献者搞不好就会改错分支,或者是错误地领会代码实施意图。冒着这些风险,只为少敲几下键盘,得不偿失。
而且,虽然就上面的代码来看,有没有花括号,代码逻辑都一样,但有些情况下并非如此。如果某个分支有多条语句响应,那么丢掉花括号的话,只有第一条语句会执行。以下是一个例子:
var arrowsInQuiver = 2 if (arrowsInQuiver >= 5) { println("Plenty of arrows") println("Cannot hold any more arrows") }
上述代码的逻辑是,如果英雄拥有箭的数目大于等于5,他就有很多了,再多就没法拿了。现在,他只有2支箭,所以控制台不会输出任何结果。但是,丢掉花括号后,你再看看:
var arrowsInQuiver = 2 if (arrowsInQuiver >= 5) println("Plenty of arrows") println("Cannot hold any more arrows")
没有花括号,第二条
println
语句就不再是if
分支的一部分了。arrowsInQuiver
变量值至少为5时,控制台才输出"Plenty of arrows"
语句,而不管arrowsInQuiver
变量值是多少,"Cannot hold any more arrows"
语句都会输出。对于单行条件表达式,想想以后看代码的人会认为哪种代码编写方式最清楚、最好理解。通常,对于单行条件表达式,不带花括号的代码更易读。例如,在NyetHack应用中,光环判断代码就是如此。或者看以下例子:
val healthSummary = if (healthPoints != 100) "Need healing!" else "Looking good."
顺便一提,你可能在想:“你说的我都明白,但我就是不喜欢
if/else
语法,即使是不带花括号的版本。这种代码风格丑疯了!”呃,不要苦恼。马上,你就会看到,健康状况表达式代码还有更简单、更清晰的写法。
3.2 range
基于healthPoints
整数值,你在if/else
表达式中设置了各个条件分支,以判断出healthStatus
变量值。这些分支中,有些使用==
操作符检查healthPoints
变量值是否等于某个固定值。有些组合使用多个比较运算符检查healthPoints
变量值是否介于两个数字之间。对于第二种情况,即一系列线性数值,Kotlin提供的range更好用。
在in 1..5
中,..
是一种操作符,表示某个范围(range)。范围包括从..
操作符左侧的值到..
操作符右侧值的一系列值。所以,1..5
包括1、2、3、4、5。除了数字,范围也可以是一系列字符。
代码片段in 1..5
中,in
关键字用来检查某个值是否在指定范围之内。重构使用比较运算符的条件表达式,改用range来判断healthStatus
值,如代码清单3-9所示。
代码清单3-9 使用range重构
healthStatus
求值(Game.kt)
fun main(args: Array<String>) { ... val healthStatus = if (healthPoints == 100) { "is in excellent condition!"} else if (healthPoints >= 90) {} else if (healthPoints in 90..99) { "has a few scratches."} else if (healthPoints >= 75) {} else if (healthPoints in 75..89) { if (isBlessed) { "has some minor wounds but is healing quite quickly!" } else { "has some minor wounds." }} else if (healthPoints >= 15) {} else if (healthPoints in 15..74) { "looks pretty hurt." } else { "is in awful condition!" } }
小福利:在条件表达式中使用range,解决了前面else if
需要排序的问题。现在,各个分支不讲顺序,随便写,结果都一样。
除了..
操作符,Kotlin还有好几个表示范围的函数。例如,函数downTo
创建降序范围,函数until
创建不包括上限值的范围。章末的挑战练习就会用到类似的函数,第10章还会深入学习range知识。
3.3 when
表达式
when
表达式是Kotlin的另一个控制流工具。类似于if/else
语句,when
表达式允许你编写条件式,在某个条件满足时,就执行对应的代码。它的语法比较简洁,非常适合有三到四个分支的情况。
以NyetHack应用程序为例,玩家可能属于orc或gnome等种族中的一个。他们按派别结成同盟。使用when
表达式,就能以族类来确定他们的派别。
val race = "gnome" val faction = when (race) { "dwarf" -> "Keepers of the Mines" "gnome" -> "Keepers of the Mines" "orc" -> "Free People of the Rolling Hills" "human" -> "Free People of the Rolling Hills" }
上述代码中,首先定义了一个race
只读变量。然后定义了一个faction
只读变量,它的值由when
表达式决定。表达式先检查race
值,判断它是否等于->
操作符(叫作箭头)左边的值,匹配的话,就将->
操作符右边的值赋给faction
变量。(后面你会看到,与其他语言不同,Kotlin中的->
操作符有自己特别的用法。)
默认情况下,when
表达式的工作原理就好比是,圆括号中的值参(argument)和花括号中的一个个条件中间有个==
操作符。(值参就是传入代码的数据,详见第4章。)
上例中,race
就是值参,所以,编译器使用race
的"gnome"
值和第一个条件做比较,不匹配就返回false结果,然后再看下一个条件。刚好,下一分支匹配,于是"Keepers of the Mines"
就赋给了faction
变量。
上例已展示了when
表达式的用法,你可以优化healthStatus
健康状况报告的代码了。相比以前的if/else
语句,when
表达式能让代码更简洁易读。实践经验表明,只要代码包含else if
分支,都建议改用when
表达式。
参照代码清单3-10,使用when
表达式重写healthStatus
健康状况逻辑。
代码清单3-10 使用
when
表达式重写healthStatus
逻辑(Game.kt)
fun main(args: Array<String>) { ...val healthStatus = if (healthPoints == 100) {"is in excellent condition!"} else if (healthPoints in 90..99) {"has a few scratches."} else if (healthPoints in 75..89) {if (isBlessed) {"has some minor wounds but is healing quite quickly!"} else {"has some minor wounds."}} else if (healthPoints in 15..74) {"looks pretty hurt."} else {"is in awful condition!"}val healthStatus = when (healthPoints) { 100 -> "is in excellent condition!" in 90..99 -> "has a few scratches." in 75..89 -> if (isBlessed) { "has some minor wounds but is healing quite quickly!" } else { "has some minor wounds." } in 15..74 -> "looks pretty hurt." else -> "is in awful condition!" } }
在定义条件和执行分支方面,when
表达式和if/else
语句类似。但在作用域(scope)方面,when
的值参能自动去和左边各条件分支匹配,也就是说能作用到所有左边的条件分支。作用域的概念还会在第4章和第12章详谈。现在,先以上例中的in 90..99
条件分支为例简单介绍一下。
你已学会使用in
关键字检查某个值是否在范围内。这里,虽然没指出名字,但代码就是在检查healthPoints
变量值。因为->
操作符左边的范围就在healthPoints
作用范围内,所以编译器计算when
表达式的值时,就当healthPoints
已包括在每一个分支条件里。
通常来讲,when
的逻辑表现力更强,代码更简洁。就上例来说,为实现同样的结果,if/else
语句需要三个else if
分支。
另外,就条件和分支匹配来说,在使用上when
表达式比if/else
语句更灵活。大部分左边的分支条件都要判断出true或false来,只有少数像100
那条分支那样,直接是全等于判断。而像上面的例子,when
表达式可以罗列每一个比较值。
顺便要说的是,注意到when
表达式某个分支里的嵌套if/else
了吗?这种用法并不常见,但Kotlin的when
表达式的特点就是灵活,就看你怎么用了。
最后,运行NyetHack应用,确保healthStatus
代码的when
表达式重写版功能如旧。
3.4 string模板
你已经看到,字符串能和变量值,甚至是条件表达式的结果值组合成新的字符串。Kotlin的string模板功能能简化这个常见任务,让代码更易读。模板支持在字符串的引号内放入变量值。参照代码清单3-11,修改玩家状态代码,用上string模板。
代码清单3-11 使用string模板(Game.kt)
fun main(args: Array<String>) { ... // Player statusprintln(name + " " + healthStatus)println("$name $healthStatus") }
使用美元$
符号作为前缀,name
和healthStatus
变量的值就添加到玩家状况字符串中了。Kotlin的这个特殊符号是一种便利,让你在字符串定义中用上了两个变量值模板。模板值会自动出现在你定义的字符串中。
运行应用程序,你应该看到和以前同样的结果。
GREEN Madrigal has some minor wounds but is healing quite quickly!
Kotlin还支持在字符串里计算表达式的值并插入结果(把结果插入当前字符串)。添加在${}
中的任何表达式,都会作为字符串的一部分求值。如代码清单3-12所示,为练习使用string模板,在玩家状况报告里添加光环颜色和运气情况。别忘了删除原来的光环颜色输出语句。
代码清单3-12 格式化输出
isBlessed
状态(Game.kt)
fun main(args: Array<String>) { ... // Aura val auraVisible = isBlessed && healthPoints > 50 || isImmortal val auraColor = if (auraVisible) "GREEN" else "NONE"print(auraColor)... // Player status println("(Aura: $auraColor) " + "(Blessed: ${if (isBlessed) "YES" else "NO"})") println("$name $healthStatus") }
新加代码行告诉编译器:输出(Blessed:
和if (isBlessed) "YES" else "NO"
表达式的结果字符串。为了简洁,写成一行的表达式省掉了花括号。它实际等同于以下代码:
if (isBlessed) { "YES" } else { "NO" }
虽然作用一样,但语法更复杂,还不如简单一点。不管哪种写法,string模板都会把表达式结果值放入字符串里。运行应用程序前,自己脑补下结果,再运行应用程序进行确认。
目前为止,程序做的事情大多是玩家状况和行为判断。这一章,我们学习了if/else
语句和when
表达式,知道了如何为代码执行添加规则。还学习了赋值版if/else
,即if/else
条件表达式。接着学习了如何使用range表示一系列数字或字符。最后学习了如何使用string模板方便地在字符串中插入变量值。
结束本章学习前,记得保存NyetHack,因为后面还会用到它。下一章,我们开始学习函数,一种在应用程序中组织、复用代码的编程方式。
3.5 挑战练习:range研究
Kotlin的range工具很强大。多用用,你就会知道它的语法有多直观。本挑战很简单,就是使用Kotlin REPL研究range语法,练习使用toList()
、 downTo
和 until
这3个函数。打开Kotlin REPL(Tools → Kotlin → REPL),输入代码清单3-13所示的代码片段(一次一行)。按Command-Return (Ctrl-Return)组合键执行之前,先思考一下会有什么样的结果。
代码清单3-13 range研究(REPL)
1 in 1..3 (1..3).toList() 1 in 3 downTo 1 1 in 1 until 3 3 in 1 until 3 2 in 1..3 2 !in 1..3 'x' in 'a'..'z'
3.6 挑战练习:优化玩家光环展示
这个练习和下一个练习都要用到NyetHack项目,所以开始前,先将项目复制一份,以免将修改内容带到后面。将复制的项目命名为NyetHack_ConditionalsChallenges,或者你自己随意取个名字。做后续各章的练习时,相信你也会这么做。
当前,玩家光环都是绿色的。请完成此挑战练习,让玩家光环颜色反映出当前karma值。
karma值的取值范围是0~20。为计算玩家的karma值,使用以下公式:
val karma = (Math.pow(Math.random(), (110 - healthPoints) / 100.0) * 20 ).toInt()
按照下表中的规则显示光环颜色。
karma值 |
光环颜色 |
---|---|
0~5 |
red(红色) |
6~10 |
orange(橘黄色) |
11~15 |
purple(紫色) |
16~20 |
green(绿色) |
使用上面的公式计算玩家的karma值,再使用条件表达式确定玩家的光环颜色。最后,修改玩家状况展示,只要光环可见,就显示正确的颜色。
3.7 挑战练习:可配置的玩家状况报告格式
当前,玩家状况报告是靠调用两个println
函数产生的。当然,也没只用一个变量来存储全部的玩家状况信息。
原来的代码是这样的:
// Player status println("(Aura: $auraColor) " + "(Blessed: ${if (isBlessed) "YES" else "NO" })") println("$name $healthStatus")
输出结果是这样的:
(Aura: GREEN) (Blessed: YES) Madrigal has some minor wounds but is healing quite quickly!
这个练习有点难,需要你使用状况格式化字符串,实现可配置的玩家状况报告格式。使用字符B
代表运气好坏,A
代表光环颜色,H
代表healthStatus
,HP
代表healthPoints
。以下是状况格式化字符串的示例:
val statusFormatString = "(HP)(A) -> H"
它应该输出这样的玩家状况报告:
(HP: 100)(Aura: Green) -> Madrigal is in excellent condition!