Kotlin 控制流

无论使用哪种编程语言,应用程序开发在很大程度上都是逻辑的应用过程,而编程的核心技巧之一就是编写能基于一个或多个条件做出决策的代码。这些决策决定了程序运行时哪些代码会被执行、执行多少次,以及反过来,哪些代码会被跳过。这通常被称为 “控制流”,因为它控制着程序的执行流程。

控制流通常分为两类:循环控制(代码执行的次数)和条件控制流(代码是否执行)。

循环控制流

本章将从循环形式的控制流开始讲解。循环本质上是 Kotlin 语句序列,会重复执行直到满足指定条件。我们要探讨的第一种循环语句是 for 循环。

for-in 语句

for-in 循环用于遍历集合或数值范围中包含的一系列元素。

其语法如下:

for ( <variable name> in <collection or range> ) { 
	// 要执行的代码
}

在该语法中,variable name 是一个变量的名称,用于存储循环当前遍历到的集合或范围中的元素。循环体中的代码通常会使用这个变量名来引用当前循环周期中的元素。collection or range 指的是循环要遍历的对象,例如字符串数组、范围运算符(…)表示的范围,甚至是一个字符串的字符序列。

示例 1:遍历数值范围,使用以下 for-in 循环结构:

for (index in 1..5) {  
    println("Value of index is $index")  
}  

该循环首先声明将当前元素赋值给名为 index 的变量,然后通过闭区间范围运算符 1…5 指定循环要遍历 1 到 5 的数值范围。循环体的作用是向控制台打印 index 的当前值,输出结果如下:

Value of index is 1  
Value of index is 2  
Value of index is 3  
Value of index is 4  
Value of index is 5  

示例 2:遍历字符串字符。for-in 循环在处理数组等集合时尤为实用,实际上,它可以遍历任何包含多个元素的对象。例如,以下循环会输出指定字符串中的每个字符:

for (index in "Hello") {  
    println("Value of index is $index")  
}  

设置循环方向与范围。for-in 循环的行为可通过 downTo 和 until 函数进行配置。其中,downTo 函数用于使循环从指定数值反向遍历到目标数值。例如,以下循环从 100 倒数到 90:

for (index in 100 downTo 90) {  
    print("$index.. ")  
}  

执行后,该循环的输出结果为:

100.. 99.. 98.. 97.. 96.. 95.. 94.. 93.. 92.. 91.. 90..  

until 函数的用法与之类似,但计数从范围的起始值开始,向上遍历到不包含指定终点的值(这种范围称为 “半开区间”):

for (index in 1 until 10) {  
    print("$index.. ")  
}  

上述代码的输出结果从起始值 1 到 9(不包含 10):

1.. 2.. 3.. 4.. 5.. 6.. 7.. 8.. 9..  

循环每次迭代的步长(增量)还可以通过 step 函数定义,例如:

for (index in 0 until 100 step 10) {  
    print("$index.. ")  
}  

上述代码的控制台输出为:

0.. 10.. 20.. 30.. 40.. 50.. 60.. 70.. 80.. 90..  
while 循环

前面介绍的 Kotlin for 循环适用于提前已知任务需要重复多少次的场景。但在有些情况下,代码需要重复执行直到满足某个条件,且无法提前确定需要重复多少次才能满足该条件。为此,Kotlin 提供了 while 循环。

本质上,while 循环会在指定条件满足时重复执行一组任务。其语法定义如下:

while ( <condition> ) { 
      // Kotlin statements go here 
}

在上述语法中,condition 是一个返回 true 或 false 的表达式,当条件表达式为 true 时要执行的代码。

var myCount = 0  

while (myCount < 100) {  
    myCount++  
    println(myCount)  
}  

在这个示例中,while 表达式会判断 myCount 变量是否小于 100。如果 myCount 已经大于等于 100,大括号中的代码会被跳过,循环直接退出,不执行任何任务。

反之,如果 myCount 小于 100,则执行大括号中的代码,之后循环会回到 while 语句,再次判断 myCount 的值。这个过程会重复进行,直到 myCount 的值大于等于 100,此时循环退出。

do … while 循环

可以把 do … while 循环理解为 “倒置的 while 循环”。while 循环在执行循环体代码之前先判断条件,如果第一次判断时条件为 false,则循环体代码不会执行。而 do … while 循环适用于循环体代码至少需要执行一次的场景。例如,你可能需要遍历数组元素直到直到找到特定元素,这时至少要检查数组的第一个元素才有可能找到目标。

do … while 循环的语法如下:

do {  
    // 此处为 Kotlin 语句  
} while (<conditional expression>)  

在下面的 do … while 示例中,循环会持续执行,直到变量 i 的值等于 0:

var i = 10  

do {  
    i--  
    println(i)  
} while (i > 0)  
break 跳出循环

创建循环后,在某些条件下,可能需要在满足循环结束条件之前提前跳出循环(尤其是在创建了无限循环的情况下)。例如,可能需要持续检查网络套接字的活动状态,一旦检测到活动,很可能就需要跳出监控循环去执行其他任务。

为了跳出循环,Kotlin 提供了 break 语句,它会终止当前循环,并从循环后的代码继续执行。例如:

var j = 10  

for (i in 0..100) {  
    j += j  

    if (j > 100) {  
        break  
    }  

    println("j = $j")  
}  

在上述示例中,循环会持续执行,直到 j 的值超过 100。此时,循环会退出,程序将从循环后的下一行代码继续执行。

continue 语句

continue 语句会跳过循环中剩余的所有代码,直接将执行流程返回到循环的开头(重新开始下一次迭代)。

在下面的示例中,println 函数仅在变量 i 的值为偶数时才会被调用:

var i = 1  

while (i < 20) {  
    i += 1  

    if (i % 2 != 0) {  // 如果 i 是奇数  
        continue       // 跳过后续代码,回到循环开头  
    }  

    println("i = $i")  // 仅偶数时执行  
}  

上述示例中,除非 i 能被 2 整除(即 i 是偶数),否则 continue 语句会跳过 println 的调用。一旦触发 continue,程序会直接跳回到 while 循环的开头,重复执行循环体中的语句(直到 i 的值超过 19 为止)。

break 和 continue 的 Label

Kotlin 中可以为表达式添加 label,方式是在表达式前加上 “标签名 + @”。之后,在使用 break 和 continue 语句时,就可以通过引用该标签来指定程序继续执行的位置。这在跳出嵌套循环时尤为实用。

以下代码包含一个嵌套的 for 循环,内层循环中有一个 break 语句,当 j 的值达到 10 时执行:

for (i in 1..100) {  
    println("Outer loop i = $i")  

    for (j in 1..100) {  
        println("Inner loop j = $j")  
        if (j == 10) break  
    }  
}  

在当前代码中,break 语句只会退出内层 for 循环,然后程序会回到外层 for 循环的开头继续执行。

但如果需要 break 语句同时退出外层循环,可以为外层循环添加标签,并在 break 语句中引用该标签,

outerloop@ for (i in 1..100) {  // 为外层循环添加标签 outerloop@  
    println("Outer loop i = $i")  

    for (j in 1..100) {  
        println("Inner loop j = $j")  
        if (j == 10) break@outerloop  // 引用标签,同时退出内外层循环  
    }  
}  

现在,当变量 j 的值达到 10 时,break@outerloop 会同时跳出两层循环,程序将从外层循环之后的代码继续执行。

条件控制流

由于编程在很大程度上是逻辑的应用过程,编程的核心技巧之一就是编写能基于一个或多个条件做出决策的代码。这些决策决定了程序运行时哪些代码会被执行,以及反过来,哪些代码会被跳过。

if 表达式

if 表达式或许是 Kotlin 程序员可用的最基础的控制流选项。熟悉 C、Swift、C++ 或 Java 的开发者会很快适应 Kotlin 的 if 语句,不过两者之间存在一些细微差异。

Kotlin 中 if 表达式的基本语法如下:

if (<boolean expression>) {  
    // 当表达式为 true 时执行的 Kotlin 代码  
}  

需要注意的是与某些编程语言不同,在 Kotlin 中,如果 if 表达式仅关联一行代码,大括号是可选的。实际上,这种情况下语句通常会与 if 表达式写在同一行。

本质上,若布尔表达式的结果为 true,则执行语句体中的代码;反之,若表达式为 false,则跳过语句体中的代码。

例如,若需要根据一个值是否大于另一个值做出决策,可编写如下代码:

val x = 10  
if (x > 9) println("x is greater than 9!")  

显然,x 确实大于 9,因此这条消息会显示在控制台中。

我们一直称其为 “if 表达式” 而非 “if 语句”,原因是Kotlin 的 if 会返回一个结果。这使得 if 结构可以在表达式中使用。例如,要找出两个数中的较大值并将结果赋值给变量,典型的 if 表达式如下:

if (x > y)  
    largest = x  
else  
    largest = y  

使用表达式中的 if 语句,也可通过以下语法实现相同结果:

<variable> = if ( <condition> ) <return_val_1> else <return_val_2>

因此,上面的示例可改写为:

val largest = if (x > y) x else y  

这种用法不仅限于返回条件中包含的值。以下示例也是 if 在表达式中的有效用法,这里是将字符串值赋给变量:

val largest = if (x > y) "x is greatest" else "y is greatest"  
println(largest)  

对于熟悉 Java 等编程语言的开发者来说,if 表达式的这一特性使得 Kotlin 中可以实现类似三元运算符(condition ? val1 : val2)的代码结构。

if else 表达式

if 表达式的另一种形式允许我们指定当 if 中的表达式结果为 false 时要执行的代码。其语法如下:

if (<boolean expression>) {  
    // 表达式为 true 时执行的代码  
} else {  
    // 表达式为 false 时执行的代码  
}  

同样,如果只需执行一行代码,大括号是可选的。

使用上述语法,我们可以扩展之前的示例,在比较表达式结果为 false 时显示不同的消息:

val x = 10  
if (x > 9) println("x is greater than 9!") else println("x is less than 10!")

在这个例子中,如果 x 的值小于或等于 9,就会执行第二个 println 语句。

if else if 表达式

到目前为止,我们讨论的 if 语句都是基于单个逻辑表达式的结果做决策。但有时需要根据多个不同的条件进行判断,这时可以使用 if … else if … 结构。

var x = 9  
if (x == 10) println("x is 10")  
else if (x == 9) println("x is 9")  
else if (x == 8) println("x is 8")  
else println("x is less than 8")  
when 语句

Kotlin 的 when 语句类似于许多其他编程语言中的 switch 语句。 when 语句为 if … else if … 结构提供了更简洁的替代方案,其语法如下:

when (value) {  
    match1 -> // 匹配 match1 时执行的代码  
    match2 -> // 匹配 match2 时执行的代码  
    // ...  
    else -> // 无匹配时执行的默认代码  
}  

使用这种语法,前面的 if … else if … 结构可以改写成 when 语句:

when (x) {  
    10 -> println("x is 10")  
    9 -> println("x is 9")  
    8 -> println("x is 8")  
    else -> println("x is less than 8")  
}  

Kotlin 函数

Kotlin 函数与 Lambda 是编写结构清晰、高效代码的重要组成部分,它们为程序组织提供了方式,同时避免了代码重复。

函数是一个命名的代码块,可被调用以执行特定任务。它可以接收用于执行任务的数据,并能向调用它的代码返回结果。

例如,若 Kotlin 程序中需要执行某个特定的算术计算,可将实现该计算的代码放入一个函数中。这个函数可以设计为接收计算所需的值(称为形参),并返回计算结果。当程序中任何地方需要该计算时,只需调用这个函数,传入参数值(称为实参),即可获得返回的结果。

在讨论函数时,“形参”(parameter)和 “实参”(argument)这两个术语常被混用,但两者存在细微区别:函数定义时声明的可接收值称为形参;而在调用函数时实际传入的值,则称为实参。

声明函数

Kotlin 函数的声明语法如下:

fun <函数名> (<参数名>: <参数类型>, <参数名>: <参数类型>, ... ): <返回类型> {  
    // 函数代码  
}  

函数名、参数和返回类型的组合称为函数签名(或函数类型)。函数声明各部分的说明如下:

  • fun:声明函数关键字,用于告知 Kotlin 编译器这是一个函数。
  • <函数名>:为函数分配的名称,在应用代码中调用函数时将通过该名称引用。
  • <参数名>:在函数代码中引用该参数时使用的名称。
  • <参数类型>:对应参数的数据类型。
  • <返回类型>:函数返回结果的数据类型。如果函数不返回结果,则无需指定返回类型。
  • 函数代码:执行具体任务的函数体代码。

示例 1:无参数、无返回值的函数

fun sayHello() {  
    println("Hello")  
}  

示例 2:带参数、有返回值的函数

fun buildMessageFor(name: String, count: Int): String {  
    return("$name, you are customer number $count")  
}  

调用函数

函数声明后,可使用以下语法调用:

<函数名>(<实参1>, <实参2>, ...)  

传递给函数的每个实参必须与函数声明的参数相匹配。例如,调用一个名为 sayHello、无参数且无返回值的函数,代码如下:

sayHello()  

对于接收参数的函数(如 buildMessageFor),调用方式如下:

buildMessageFor("John", 10)  

如果指明了形参名称,则形参顺序可以变动。

fun main() {
    sub(num2 = 10, num1 = 20)
}

fun sub(num1: Int, num2: Int): Int {
    return num1 - num2
}

单行表达式函数

当函数仅包含单个表达式时,无需用大括号包裹该表达式。只需在函数声明后加一个等号(=),后面直接跟表达式即可。

以下函数包含单个表达式,采用常规方式声明:

fun multiply(x: Int, y: Int): Int {  
    return x * y  
}  

下面是用单行表达式形式编写的同一个函数:

fun multiply(x: Int, y: Int): Int = x * y  

使用单行表达式时,如果编译器能推断出表达式的返回类型,可省略返回类型声明,使代码更简洁:

fun multiply(x: Int, y: Int) = x * y  

局部函数

局部函数是嵌套在另一个函数内部的函数。此外,局部函数可以访问其外部函数中包含的所有变量:

fun main(args: Array<String>) {  
    val name = "John"  
    val count = 5  

    fun displayString() {  // 局部函数,嵌套在 main 函数内部  
        for (index in 0..count) {  
            println(name)  // 访问外部函数的变量 name 和 count  
        }  
    }  

    displayString()  // 调用局部函数  
}  

函数返回值

对于调用一个名为 buildMessageFor、接收两个参数并返回结果的函数,我们可以编写如下代码:

val message = buildMessageFor("John", 10)  

为提高代码可读性,调用函数时也可以显式指定参数名:

val message = buildMessageFor(name = "John", count = 10)  

在上述示例中,我们创建了一个名为 message 的新变量,然后使用赋值运算符(=)存储函数返回的结果。

声明函数指定默认参数

Kotlin 允许为参数指定默认值,当函数调用时未传入该参数的实参时,就会使用这个默认值。只需在声明函数时为参数赋值即可指定默认值。

为了演示默认参数的用法,我们修改 buildMessageFor 函数:如果未传入客户姓名,就使用字符串 “Customer” 作为默认值;同理,count 参数的默认值设为 0。

fun buildMessageFor(name: String = "Customer", count: Int = 0): String {  
    return("$name, you are customer number $count")  
}  

调用函数时如果指定了参数名,那么任何有默认值的参数都可以省略。例如,下面的调用省略了客户姓名参数,但由于为第二个参数指定了参数名,代码仍能正常编译:

val message = buildMessageFor(count = 10)  

不过,如果调用时不指定参数名,则只能省略末尾的参数:

val message = buildMessageFor("John") // 有效(省略了末尾的 count 参数)  
val message = buildMessageFor(10)     // 无效(10 会被当作 name 参数的值,类型不匹配)  

函数的可变参数

在应用代码中调用函数时,往往无法提前确定函数需要接收的参数数量。Kotlin 通过 vararg 关键字解决这一问题,它用于函数可以接收任意数量的指定数据类型参数。在函数体内,这些参数以数组对象的形式存在。

例如,以下函数接收可变数量的 String 类型参数,并将它们输出到控制台:

fun displayStrings(vararg strings: String) {  
    for (string in strings) {  
        println(string)  
    }  
}  

调用该函数时可传入任意多个字符串参数:

displayStrings("one", "two", "three", "four")  

Kotlin 不允许一个函数中声明多个 vararg 参数,且函数的普通参数必须声明在 vararg 参数之前:

fun displayStrings(name: String, vararg strings: String) {  // 普通参数 name 在 vararg 之前  
    println(name)  

    for (string in strings) {  
        println(string)  
    }  
}  

Lambda 表达式

在学习了 Kotlin 函数的基础知识后,现在来了解 Lambda 表达式的概念。本质上Lambda 是自包含的代码块。

例如,以下代码声明了一个 Lambda,将其赋值给变量 sayHello,然后通过该 Lambda 引用调用函数:

val sayHello = { println("Hello") }  
sayHello()  

Lambda 表达式也可以配置为接收参数并返回值,其语法如下:

{ <参数名>: <参数类型>, <参数名>: <参数类型>, ... ->  
    // Lambda 表达式内容  
}  

例如,下面的 Lambda 表达式接收两个整数参数并返回一个整数结果:

val multiply = { val1: Int, val2: Int -> val1 * val2 }  
val result = multiply(10, 20)  // 结果为 200  

需要注意的是,上面的 Lambda 示例都将 Lambda 代码块赋值给了变量。这种方式也适用于函数,但要注意区别:

  • 下面的语法会执行函数,并将执行结果赋值给变量,而不是将函数本身赋值给变量:
val myvar = myfunction()  // 存储函数执行结果  
  • 若要将函数引用赋值给变量,只需去掉括号,并在函数名前加上双冒号(::),之后通过变量名即可调用该函数:
val myvar = ::myfunction  // 存储函数引用  
myvar()  // 调用函数  

Lambda 块可以直接执行:在表达式末尾加上括号(包含所需参数)即可。例如,下面的 Lambda 直接执行乘法运算(10 乘以 20):

val result = { val1: Int, val2: Int -> val1 * val2 }(10, 20)  // 结果为 200  

Lambda 中最后一个表达式的结果会作为整个 Lambda 的返回值(因此上面的乘法示例中,result 变量被赋值为 200)。实际上,与函数不同,Lambda 不支持 return 语句。如果是由返回值的表达式,只需将值作为 Lambda 的最后一项,该值就会被返回。

  • 以下 Lambda 打印消息后返回布尔值 true:
val result = { println("Hello"); true }()  // 结果为 true  
  • 以下 Lambda 直接返回一个字符串字面量:
val nextmessage = { println("Hello"); "Goodbye" }()  

Lambda 和函数引用的一个特别实用的特性是:它们既可以作为参数传递给函数,也可以作为结果返回。不过,要理解这一概念,需要先掌握函数类型和高阶函数的相关知识。

Lambda 代码示例

语法讲解::(参数类型1, 参数类型2) -> 这一段声明输入参数,可省略;= { 函数体 } 为函数体代码,单行表达式可省略花括号。

:(参数类型1, 参数类型2) -> 返回值类型 = { 函数体 }

示例1:无输入参数,无返回值,无函数体。

val result: () -> Unit

示例2:一个输入参数,无返回值,无函数体。

val result: (Int) -> Unit

示例3:二个输入参数,无返回值,无函数体。

val result: (Int, Int) -> Unit

示例4:二个输入参数,有返回值,无函数体。

val result: (Int, Int) -> Int

示例5:无输入参数,无返回值,有函数体。

val result: () -> Unit = { print("Hello Lambda") }

====== 省略声明参数类型 ======

val result = { print("Hello Lambda") }

示例6:两个输入参数,有返回值,有函数体。

val result: (Int, Int) -> Int = { num1: Int, num2: Int -> num1 + num2 }

====== 声明了输入类型,所以函数体可省略输入类型:Int ======

val result: (Int, Int) -> Int = { num1, num2 -> num1 + num2 }

====== 如果省略声明参数类型,则函数体就要指明输入参数类型 ======

val result = { num1: Int, num2: Int -> num1 + num2 }

示例7:申明和实现分开的方式。两个输入参数,有返回值,有函数体。

val result: (Int, Int) -> Int
result = { num1: Int, num2: Int -> num1 + num2 }
// result = { num1, num2 -> num1 + num2 } 可省略 : int,因为已经声明参数类型

====== 或者 ======

val result: (Int, Int) -> Int
result = fun(num1: Int, num2: Int): Int = num1 + num2
// result = fun(num1, num2): Int = num1 + num2 可省略 : int,因为已经声明参数类型

示例8:申明和实现合并的方式。两个输入参数,有返回值,有函数体。

val result: (Int, Int) -> Int = fun(num1, num2) = num1 + num2

示例9:申明和实现合并的方式。两个输入参数,有返回值,有函数体。

val result: (Int, Int) -> Int = fun(num1, num2) = num1 + num2

示例10:只有一个参数可用 it 代替。一个输入参数,无返回值,有函数体。

val result: (String) -> Unit = { it -> print("传入数据:$it") }

====== it -> 可省略 ======

val result: (String) -> Unit = { print("传入数据:$it") }

示例10:用 _ 拒绝收值。两个个输入参数,无返回值,有函数体。

val result: (String, String) -> Unit = { _, s2 -> print("传入数据:$s2") }

高阶函数

表面上看,Lambda 和函数引用似乎并不是特别引人注目的特性。但当我们意识到它们与许多其他数据类型具有相同的能力时(尤其是可以作为参数传递给另一个函数,甚至作为结果从函数返回),这些特性的价值就变得更加明显了。

高阶函数指的是能够接收函数或 Lambda 作为参数,或者返回函数或 Lambda 作为结果的函数。

在探讨 “将一个函数嵌入另一个函数” 的能力之前,首先需要理解函数类型的概念。函数的类型由它接收的参数类型和返回的结果类型共同决定。例如,一个接收 Int 和 Double 类型参数、返回 String 类型结果的函数,其函数类型为:

(Int, Double) -> String  

若一个函数要接收另一个函数作为参数,只需声明它所能接受的函数类型即可。

举个例子,我们先声明两个单位转换函数:

fun inchesToFeet(inches: Double): Double {  
    return inches * 0.0833333  
}  

fun inchesToYards(inches: Double): Double {  
    return inches * 0.0277778  
}  

现在需要一个额外的函数,用于执行单位转换并在控制台打印结果。这个函数应尽可能通用,能够支持多种不同的计量单位转换。为了演示 “函数作为参数” 的用法,这个新函数将接收两个参数:一个与 inchesToFeet 和 inchesToYards 函数匹配的函数类型,以及一个待转换的值。由于这两个转换函数的类型都是 (Double) -> Double,我们的通用函数可以这样编写:

fun outputConversion(value: Double, converterFunc: (Double) -> Double) {  
    val result = converterFunc(value)  // 调用传入的转换函数  
    println("Result of conversion is $result")  
}  

调用 outputConversion 函数时,需要传入一个与声明类型匹配的函数。该函数会被调用以执行转换,结果将显示在控制台中。这意味着只需传入合适的转换函数作为参数,同一个 outputConversion 函数就能同时用于将英寸转换为英尺和码(注意:传递的是函数引用)。

outputConversion(24, ::inchesToFeet)   // 转换为英尺  
outputConversion(24, ::inchesToYards)  // 转换为码  

函数也可以作为返回值,只需将函数类型声明为返回类型即可。以下函数根据布尔参数的值,返回 inchesToFeet 或 inchesToYards 函数(即接收并返回 Double 类型的函数):

fun decideFunction(feet: Boolean): (Double) -> Double {  
    if (feet) {  
        return ::inchesToFeet  
    } else {  
        return ::inchesToYards  
    }  
}  

调用该函数时,会返回一个函数引用,可用于执行转换:

val converter = decideFunction(true)  // 获取转换为英尺的函数  
val result = converter(22.4)          // 调用函数执行转换  
println(result)                       // 输出结果  

小结

“控制流” 用于描述程序运行时决定源代码执行路径的逻辑。本章介绍了 Kotlin 提供的两种控制流类型(循环控制流和条件控制流),并探讨了实现这两种控制流逻辑的各类 Kotlin 结构。

函数和 Lambda 表达式是自包含的代码块,可被调用以执行特定功能,它们为代码结构化和复用提供了机制。本章介绍了函数与 Lambda 的声明和实现的基本概念,此外还讲解了高阶函数的使用,高阶函数是指将 Lambda 和 Function 作为参数或返回值。

更多推荐