七、结构性构造

从计算机语言一开始,程序流的条件分支就是程序代码必须能够表达的最基本的东西之一。这种分支发生在函数内部,因此它在类和单例对象内部强加了某种子结构。在这一章中,我们将介绍这样的分支结构,以及帮助我们编写相应代码的辅助类。

如果和何时

在现实生活中,许多行为都是基于决策的。如果满足某些条件,动作 A 发生;否则,会发生动作 B。对于任何编程语言,我们都需要类似的东西,而创建这种程序流分支的最基本方法是古老的 if–else if–else 结构。你检查某个条件,如果满足,if 分支被执行。如果不是,可以选择检查另一个 else if 条件,如果满足这个条件,就执行相应的分支。在可能更多的 else if 子句之后,如果 if 和 else if 检查都没有产生true,则执行最后一个 else 块。

当然,在 Kotlin 中,我们有这样一个 if-else if-else 程序结构,它是这样的

if( [condition] ) {
   [statements1]
} else if( [condition2] ) {
   [statements2]
} else if( [condition3] ) {
   [statements3]
... more "else ifs"
} else {
   [statementsElse]
}

其中所有 else if 和 else 子句都是可选的,并且每个条件的计算结果都必须是布尔值。如何计算这个值取决于你:它可以是一个常数,一个变量,或一个复杂的表达式。作为一个例子,考虑检查某个变量v是否等于某个特定的常数,如果是,调用某个函数abc1()。如果没有,调用函数abc2()代替。代码如下:

if( v == 7 ) {
   abc1()
} else {
   abc2()
}

如果块只包含一条语句,可以省略花括号甚至换行符,所以

if( v == 7) abc1() else abc2()

一行是有效代码。

作为一种特性,类似于 Kotlin 中的大多数其他构造,这样的条件构造可以有一个值,因此可以在表达式中使用。为此,所有语句块的最后一行必须计算相应的数据。

val x = if( [condition] ) {
   [statements1]
   [value1]
} else if( [condition2] ) {
   [statements2]
   [value2]
} else if( [condition3] ) {
   [statements3]
... more "else ifs"
} else {
   [statementsElse]
   [valueElse]
}

这一次 else 子句不是可选的;否则,如果没有 else 值,则完整构造的结果是未定义的。不用说,块末尾的所有值都必须具有相同的期望类型,这个结构才能工作。

类似于非表达式变量,如果块中没有语句,可以省略括号和换行符,因此这是一个有效的语句:

val x = if( a > 3 ) 27 else 28

带有大量 else if 子句的大型条件分支结构相当笨拙。这就是为什么有另一个更简洁的结构,其内容如下:

when( [expression] ) {
   val1 -> { ... }
   val2 -> { ... }
   ...
   else -> { ... }
}

[expression]->前面给出值时,分支{}被执行。这个也能评估出一个值:

val x = when( [expression] ) {
   val1 -> { ... }
   val2 -> { ... }
   ...
   else -> { ... }
}

其中每个{}中的最后一个元素将被用作在相应的检查匹配时返回的值。

为了避免代码块的重复,您还可以定义评估组,如

when( [expression] ) {
   val1      -> { ... }
   val2,val3 -> { ... }
   ...
   else -> { ... }
}

这也适用于价值产出型。

对于->左侧的值,您可以使用任意表达式,包括函数调用:

val x = when( [expression] ) {
   calc(val1) + 7 -> { ... }
   val2,val3      -> { ... }
   ...
   else           -> { ... }
}

此外,我们可以使用一个特殊的in操作符或者它的反操作符!in来进行包含检查:

val l = listOf(...)
val x = when( [expression] ) {
   in l         -> { ... }
   in 27..53    -> { ... }
   !in 100..110 -> { ... }
   ...
   else         -> { ... }
}

这也适用于数组。27..53100..110定义了范围,表示它们代表了给定的极限值和之间的所有值。我们将在下一节更详细地讨论范围。

另一个方便的检查是一个特殊的is操作符,它执行类型检查:

val q:Any = ... // any type
val x = when(q) {
   is Int       -> { ... }
   is String    -> { ... }
   ...
   else         -> { ... }
}

还有一个is的否定变体:不出意外,读起来是!is

同样,对于单行代码块,可以省略括号,如下所示:

val q = ... // some Int
val x = when( q ){ 1 -> "Jean" 2 -> "Sam" else -> "" }

如果您需要来自内部when()[expression]用于内部程序块的评估,可以捕获它:

val x = when(val q = [some value]) {
   1 -> q * 3
   2 -> q * 4
   ...
   else -> 0
}

其中捕获变量仅在when块内有效。

范围

范围经常用于循环需要。我们将在下一节讨论循环,所以请将这一节视为准备步骤。范围由两个界限值和两个界限值之间的插值方式定义。

在 Kotlin 中,有三种类型的范围用于IntLongChar类型。使用构造函数,可以按如下方式构建它们:

val r1 = IntRange(1, 1000)
val r2 = LongRange(1, 10_000_000_000)
val r3 = CharRange('A', 'I')

此外,为了达到同样的目的,您可以使用范围运算符..,如下所示:

val r1 = 1..1000
val r2 = 1L..10_000_000_000L
val r3 = 'A'..'I'

最后,一些 Kotlin 标准库函数返回范围或作用于范围。任何整数类型(即ByteShortIntLongChar))都有一个rangeTo()函数来创建一个范围。因此,也可以通过编写7.rangeTo(77)来构建7..77

范围还有一个step属性,它定义了如何在范围边界之间插值。默认情况下,步长为+1,但您可以按如下方式进行调整:

1..1000 step 5
(1..1000 step 5).reversed()

其中最后一行的reversed()交换边界并否定该步骤。请注意,根据语言设计,不允许显式指定负步长。然而,允许使用downTo操作符:

1000 downTo 1 step 5

如果使用firstlast属性,范围表示第一个和最后一个值:

(1..1000 step 5).first          // -> 1
(1..1000 step 5).last           // -> 996
(1000 downTo 1 step 5).first    // -> 1000
(1000 downTo 1 step 5).last     // -> 5

For 和 While 循环

循环对应于反复迭代多次的程序部分。这种循环的一种可能是for循环,如下所示:

for( i in [loop data] ) {
    // do something with i
}

其中[loop data]是一个范围、一个集合、一个数组或任何其他具有函数iterator()的对象,返回一个具有next():EhasNext():Boolean函数的对象(E是循环变量类型)。在后一种情况下,所有三个功能iterator()next()hasNext()必须标有operator

for循环类似的还有whiledo .. while循环,它们会继续循环,直到某个条件产生false:

while( [condition] ) {
    // do something
}

do {

    // do something
} while( [condition] )

其中,在第一种情况下,在最开始时检查条件,在第二种情况下,在任何迭代(包括第一次)结束时检查条件。

forwhile循环都可以通过在内部程序流中使用break来优先退出。同样,在循环中的任何地方使用continue语句都会强制进行下一次迭代,忽略continue后面的任何内容:

while( [condition] ) {
    ...
    break // -> exit loop
    ...
    continue // -> next iteration
    ...
}

或者类似地用于fordo .. while循环。

注意

Forwhile循环现在被认为是非常老派的。在集合上使用forEach()可以更好地控制循环准备动作,比如转换和过滤,所以比起forwhile,更喜欢使用forEach()。在后面的章节中,我们会谈到很多关于集合和集合数据的迭代。

范围函数

当涉及到代码的表现力时,Kotlin 的几个标准库函数非常强大。其中的五个applyletalsorunwith被称为作用域函数,因为它们在函数内部打开了一个新的作用域,从而改善了程序流的结构。让我们看看他们做了什么,以及他们如何帮助我们写出更好的代码。

注意

顺便说一句,如果你需要一个助记符来记住它们,读读“让我们也用 APPLY 运行”

应用功能

让我们看看这些作用域函数中的第一个,apply。你可以把它挂在任何物体上,比如

object.apply {
    ...
}

这看起来并不太冒险,但是神奇的是在apply的花括号内的对象实例发生了什么:它被传输到this。另外,apply自动返回对象实例。因此,如果我们写this.somePropertysomeProperty,或this.someFunction()别名someFunction(),它指的是apply前面的object,而不是周围的上下文。这是什么意思?好吧,想想这个:

class A { var x:Int, var y:Int }
val instance = A()
instance.x = 4
instance.y = 5
instance.y *= instance.x

如果我们现在将.apply{}写在已初始化的对象后面,我们可以使用this来访问实例并获得

class A { var x:Int, var y:Int }
val instance = A().apply{
    this.x = 4
    this.y = 5
    this.y *= this.x
}

其可以进一步缩短,因为this.可以省略:

class A { var x:Int, var y:Int }
val instance = A().apply{
    x = 4
    y = 5
    y *= x
}

注意

因为propertyNamefunctionName()针对的是this实例,所以我们也可以说this代表了这种简单属性和函数访问的接收者。没有作用域函数,this指的是周围的类实例或单例对象。随着thisapply{ ... }中被重新定义,.apply前面的实例成为新的接收者。

如果在apply{}构造中使用的属性或函数标识符在 receiver 对象中不存在,则使用周围的上下文:

var q = 37
class A { var x:Int, var y:Int }
val instance = A().apply {
    x = 4
    y = 5
    q = 44 // does not exist in A, so the q from
           // outside gets used
}

apply{}被操作的对象与同一对象接收的花括号内的this作用域函数和属性之间的这种强耦合,使得apply{}构造成为在对象实例化后立即准备对象的极好候选:

val x = SomeClass().apply {
    // do things with the SomeClass instance
    // while assigning it to x
}

来自周围上下文(类或单例对象)的this不会丢失。如果您在apply{}中需要它,您可以通过添加一个限定符@Class来获得它,如

class A {
    fun goA() { ... }
    ...
    val x = SomeClass().apply {
        this.x = ...    // -> SomeClass.x
        x = ...         // -> SomeClass.x
        this@A.goA()    // -> A.goA()
        ...
    }
}

字母功能

let作用域函数经常被用来将一个对象转换成一个不同的对象。它的完整概要是这样的:

object.let { o ->
    [statements] // do s.th. with 'o'
    [value]
}

最后一行必须包含let{}应该返回的表达式。let{}构造有一个函数作为参数,如果你像这里这样写它,并使用一个匿名的 lambda 函数和o作为参数,这个参数函数获得对象本身作为参数。您也可以省略o ->,在这种情况下,会自动使用一个特殊变量it:

object.let {
    [statements] // do s.th. with 'it'
    [value]
}

注意

在花括号内写没有x ->let { },看起来好像{ }是一个功能块。这是一个句法上的巧合;实际上,它是一个匿名的 lambda 函数,以自动变量it为参数。

以其他函数为参数的函数称为高阶函数。我们将在第 12 章中讲述高阶函数。

举个简单的例子,我们取一个字符串,用let{}给它附加一个换行符"\n":

val s = "Hello World"
val s2 = s.let { it + "\n" }
// or    s.let { string -> string + "\n" }

with 函数

with作用域函数是apply{}的兄弟。不同之处在于,它只是获取要转换为接收方的对象或值作为参数:

val o = ... // some value
with(o){
    // o is now "this"
    ...
}

with函数经常用于避免重复编写要操作的对象,如

with(object){ f1(37)
    f1(12)
    fx("Hello")
}

代替

object.f1(37)
object.f1(12)
object.fx("Hello")

“也”函数

also作用域函数与apply{}函数相关,但不重新定义this。相反,它将also前面的对象或值作为参数提供给 lambda 函数参数:

object.also { obj ->
    // 'obj' is object
    ...
}

或者

object.also {
    // 'it' is object
    ...
}

您将also{ }用于横切关注点,这意味着您不改变对象(这就是apply{}的目的),但是执行与当前程序流无关的动作。执行缓存、日志记录、身份验证或在某个注册表对象中注册对象都是合适的例子。

运行功能

run作用域函数类似于apply{}函数。但是,它不返回 receiver 对象,而是返回最后一条语句的值:

val s = "Hello"
val x = s.run {
    // 'this' is 's'
    ...
    [value]
}
// x now has [value]

你可以把run{}看做一个通用的“用一个物体做点什么”的括号。不过,一个突出的用例是,只在对象不为空时才处理它。代替

var v:String? = ...
...
if(v != null) {
    ...
}

你可以写作

var v:String? = ...
...
v?.run {
    ...
}

记住,只有当前面的对象不是null时,?.才会访问一个属性或调用一个函数。在某些情况下,更简洁的后一种变体可能更具可读性。

条件执行

允许我们将条件分支编写为实例函数的结构如下所示:

someInstance.takeIf { [boolean_expression] }?.run {
    // do something
}

在布尔表达式中,您可以使用it来引用someInstance。如果布尔表达式的计算结果为true,则takeIf()函数返回接收者(这里是someInstance);否则返回null。这适用于任何对象。