第 3 章 条件语句

第 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 = 100
    var 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,用在ifelse之间。如代码清单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分支。

由此可见,条件表达式的顺序至关重要。如果你按从低到高的顺序安排ifelse 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的精心设计,让我们既能够享受到条件语句的优点,又能编写出简洁易读的代码来。下面来看几个例子。

  1. 逻辑运算符

    在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变量值即可。这和前面的代码功能等效,只不过改用变量表达式求值并赋值了。变量后面的表达式定义的规则很好读;定义了什么,看变量名便一目了然。应用程序的逻辑越来越复杂时,这种方式非常有用,它也有助于将来的代码阅读者理解你的表达意图。

    再次运行应用程序,确认代码功能和以前一样,控制台输出结果也相同。

  2. 条件表达式

    现在,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)
    ...
    

    再次运行应用程序,确保代码运行如常。你应该看到同样的输出结果,但代码更优雅易读了。

    你可能已注意到了,光环判断条件表达式的两对花括号不见了。我们一起来看看何以如此。

  3. 删除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 status
    println(name + " " + healthStatus)
    println("$name $healthStatus")
}

使用美元$符号作为前缀,namehealthStatus变量的值就添加到玩家状况字符串中了。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()downTountil这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代表healthStatusHP代表healthPoints。以下是状况格式化字符串的示例:

val statusFormatString = "(HP)(A) -> H"

它应该输出这样的玩家状况报告:

(HP: 100)(Aura: Green) -> Madrigal is in excellent condition!

目录

  • 版权声明
  • 献词
  • 致谢
  • 前言
  • 第 1 章 Kotlin应用开发初体验
  • 第 2 章 变量、常量和类型
  • 第 3 章 条件语句
  • 第 4 章 函数
  • 第 5 章 匿名函数与函数类型
  • 第 6 章 null安全与异常
  • 第 7 章 字符串
  • 第 8 章 数
  • 第 9 章 标准库函数
  • 第 10 章 List与Set
  • 第 11 章 Map
  • 第 12 章 定义类
  • 第 13 章 初始化
  • 第 14 章 继承
  • 第 15 章 对象
  • 第 16 章 接口与抽象类
  • 第 17 章 泛型
  • 第 18 章 扩展
  • 第 19 章 函数式编程基础
  • 第 20 章 Kotlin与Java互操作
  • 第 21 章 用Kotlin开发首个Android应用
  • 第 22 章 Kotlin协程简介
  • 第 23 章 编后语
  • 附录 A 补充挑战练习
  • 术语表