2.3 流程控制语句

流程控制语句是编程语言中的核心之一,可分为:

  •  分支语句(if、when)
  •  循环语句(for、while)
  •  跳转语句(return、break、continue、throw)

2.3.1 if表达式

if…else语句是控制程序流程的最基本形式,其中else是可选的。在Kotlin中,if是一个表达式,即它会返回一个值(跟Scala一样)。代码示例如下:

另外,if的分支可以是代码块,最后的表达式作为该块的值:

if作为代码块时,最后一行为其返回值。另外,在Kotlin中没有类似true?1:0这样的三元表达式。对应的写法是使用if…else语句:

    if(true) 1 else 0  //if…else实现三元表达式的逻辑

if…else语句规则:if后的括号不能省略,括号里表达式的值必须是布尔型。

代码反例:

如果条件体内只有一条语句需要执行,那么if后面的大括号可以省略。良好的编程风格是建议加上大括号。

编程实例:用if…else语句判断某年份是否是闰年。

2.3.2 when表达式

when表达式类似于switch…case表达式。when会对所有的分支进行检查直到有一个条件被满足。但相比switch而言,when语句的功能要更加强大、灵活。

Kotlin的极简语法表达风格,使得我们对分支检查的代码写起来更加简单、直接:

输出如下:

    1 ===> 这是一个0-9之间的数字
    hello ===> 这个是字符串hello
    X ===> 这是一个Char类型数据
    null ===> else类似于Java中的case-switch中的default

像if语句一样,when语句的每一个分支也可以是一个代码块,它的值是块中最后的表达式的值。如果其他分支都不满足条件会到else分支(类似default)。

如果我们有很多分支需要用相同的处理方式,则可以把多个分支条件放在一起,用逗号分隔:

    0,1,2,3,4,5,6,7,8,9 -> println("${obj} ===> 这是一个0-9之间的数字")

可以用任意表达式(而不只是常量)作为分支条件:

也可以检测一个值在in或者不在!in一个区间或者集合中:

编程实例:用when语句写一个阶乘函数。

2.3.3 for循环

for循环可以对任何提供迭代器(iterator)的对象进行遍历,语法如下:

如果想要通过索引遍历一个数组或者一个list,可以这么做:

    for (i in array.indices) {  //array.indices 存储了数组 array 的下标序列
       print(array[i])
    }

其中,array.indices持有数组的下标列表。我们也可以使用函数withIndex()来遍历下标与对应的元素:

另外,范围(Ranges)表达式也可用于循环中:

代码简写如下:

    (1..10).forEach { print(it) }

其中的操作符形式的1..10等价于1.rangeTo(10)函数调用,由in和!in进行连接。

编程实例:编写一个Kotlin程序在屏幕上输出1!+2!+3!+…+10!的和。

我们使用上面的fact()函数,代码实现如下:

2.3.4 while循环

while和do…while循环语句的使用方式与C、Java语言基本一致。代码示例如下:

2.3.5 break和continue

break和continue语句都是用来控制循环结构的,主要用来停止循环(中断跳转),但是二者又有区别,下面分别介绍。

break语句用于完全结束一个循环,直接跳出循环体,然后执行循环后面的语句。

1. 问题场景:打印数字1~10,只要遇到偶数就结束打印

下面我们使用for循环打印1~10之间的数字,遇到偶数就使用break语句结束循环。代码示例如下:

输出如下:

    1
    2

continue语句是只终止本轮循环,但是还会继续下一轮循环。可以简单理解为直接在当前语句处中断,跳转到循环入口,执行下一轮循环。而break语句则是完全终止循环,跳转到循环出口。

2. 问题场景:打印数字1~10中的奇数

下面我们使用for循环来打印1~10中的数字,遇到偶数就使用continue语句跳过本轮循环,实现只打印奇数的效果。代码示例如下:

输出如下:

    1
    3
    5
    7
    9

2.3.6 return返回

在Java和C语言中,return语句是很常见的了。虽然在Scala和Groovy类语言中,函数的返回值可以不需要显示用return语句来指定,但是我们仍然认为使用return语句的编码风格更容易阅读理解(尤其是在分支流代码块中)。

在Kotlin中,除了表达式的值,有返回值的函数都要求显式使用return语句返回其值。代码示例如下:

在Kotlin中可以直接使用“=”符号返回一个函数的值,这样的函数称为函数字面量。代码示例如下:

代码说明如下:

第(1)处使用表达式声明sum()函数。

第(2)处ifelse表达式返回的是一个值,我们直接用表达式来定义一个max()函数。

第(3)处使用fun关键字声明了一个匿名函数,并且直接使用表达式来实现函数。需要注意的是,后面的函数体语句中有没有大括号{}代表的意义完全不同。例如下面的代码:

代码说明如下:

第(4)处带上大括号{}的表达式返回的是一个Lambda表达式。

第(5)处{a+b}的类型是从(kotlin.Int, kotlin.Int)到()->kotlin.Int的映射函数。

第(6)处调用了sumf()函数。

第(7)处中sumf(1,1)的返回值是一个函数()->kotlin.Int,而不是具体的数值2。如何实现真正的函数调用呢?请看第(8)处。

第(8)处使用invoke()函数实现真正调用函数。

在Kotlin中,()操作符对应的是类的重载函数,如invoke()。我们使用()运算符来调用函数。也就是说第(8)处与第(9)处是等价的写法。

我们也可以使用fun关键字加上函数名来声明函数,下面同样是展示带上大括号{}的例子,代码示例如下:

可以看出,sumf和maxf()的返回值都是函数类型:

    () -> kotlin.Int

这点与Scala是不同的。在Scala中,带不带大括号{}的意义是一样的:

可以看出,maxf: (x: Int,y:Int)Int与maxv: (x: Int, y: Int)Int的签名是一样的。在这里,Kotlin与Scala在大括号的使用上是完全不同的。调用函数方式是直接调用invoke()函数sumf(1,1).invoke()。

Kotlin中return语句会从最近的函数或匿名函数中返回,但是在Lambda表达式中遇到return语句时,则直接返回最近的外层函数。例如下面两个函数是不同的:

输出如下:

    1
    2

遇到3时会直接返回(类似于循环体中的break语句)。

而我们给forEach传入一个匿名函数fun(a:Int),这个匿名函数里的return语句不会跳出forEach循环,有点像continue语句的效果:

输出如下:

    1
    2
    4
    5

为了显式地指明return语句返回的地址,Kotlin还提供了@Label(标签)来控制返回语句,且看2.3.7节的讲解。

2.3.7 标签(label)

在Kotlin中任何表达式都可以用标签(label)来标记。标签的格式为标识符后跟@符号,如abc@、_isOK@都是有效的标签。我们可以用Label标签来控制return、break或continue语句的跳转(jump)行为。

代码示例如下:

输出如下:

    1
    2
    4
    5

我们在Lambda表达式开头处添加了标签here@,可以这么理解:该标签相当于记录了Lambda表达式的指令执行入口地址,然后在表达式内部使用return@here跳转至Lambda表达式中的该地址处。这样代码更加易懂。

另外,也可以使用隐式标签,更加方便。该标签与接收该Lambda的函数同名。

代码示例如下:

    val intArray = intArrayOf(1, 2, 3, 4, 5)
    intArray.forEach {
        if (it == 3) return@forEach  //返回@forEach 处继续下一个循环
        println(it)
    }

输出如下:

    1
    2
    4
    5

接收该Lambda表达式的函数是forEach,所以我们可以直接使用return@forEach跳转到此处执行下一轮循环。

2.3.8 throw表达式

在Kotlin中throw是表达式,它的类型是特殊类型Nothing。该类型没有值,与C、Java语言中的void意思一样。

我们在代码中,用Nothing来标记无返回的函数:

另外,如果把一个throw表达式的值赋给一个变量,需要显式声明类型为Nothing,代码示例如下:

另外,因为ex变量是Nothing类型,没有任何值,所以无法作为参数传给函数。