七、结构性构造
从计算机语言一开始,程序流的条件分支就是程序代码必须能够表达的最基本的东西之一。这种分支发生在函数内部,因此它在类和单例对象内部强加了某种子结构。在这一章中,我们将介绍这样的分支结构,以及帮助我们编写相应代码的辅助类。
如果和何时
在现实生活中,许多行为都是基于决策的。如果满足某些条件,动作 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..53
和100..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 中,有三种类型的范围用于Int
、Long
和Char
类型。使用构造函数,可以按如下方式构建它们:
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 标准库函数返回范围或作用于范围。任何整数类型(即Byte
、Short
、Int
、Long
和Char)
)都有一个rangeTo()
函数来创建一个范围。因此,也可以通过编写7.rangeTo(77)
来构建7..77
。
范围还有一个step
属性,它定义了如何在范围边界之间插值。默认情况下,步长为+1
,但您可以按如下方式进行调整:
1..1000 step 5
(1..1000 step 5).reversed()
其中最后一行的reversed()
交换边界并否定该步骤。请注意,根据语言设计,不允许显式指定负步长。然而,允许使用downTo
操作符:
1000 downTo 1 step 5
如果使用first
或last
属性,范围表示第一个和最后一个值:
(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():E
和hasNext():Boolean
函数的对象(E
是循环变量类型)。在后一种情况下,所有三个功能iterator()
、next()
和hasNext()
必须标有operator
。
与for
循环类似的还有while
和do .. while
循环,它们会继续循环,直到某个条件产生false
:
while( [condition] ) {
// do something
}
do {
// do something
} while( [condition] )
其中,在第一种情况下,在最开始时检查条件,在第二种情况下,在任何迭代(包括第一次)结束时检查条件。
for
和while
循环都可以通过在内部程序流中使用break
来优先退出。同样,在循环中的任何地方使用continue
语句都会强制进行下一次迭代,忽略continue
后面的任何内容:
while( [condition] ) {
...
break // -> exit loop
...
continue // -> next iteration
...
}
或者类似地用于for
和do .. while
循环。
注意
For
和while
循环现在被认为是非常老派的。在集合上使用forEach()
可以更好地控制循环准备动作,比如转换和过滤,所以比起for
和while
,更喜欢使用forEach()
。在后面的章节中,我们会谈到很多关于集合和集合数据的迭代。
范围函数
当涉及到代码的表现力时,Kotlin 的几个标准库函数非常强大。其中的五个apply
、let
、also
、run
和with
被称为作用域函数,因为它们在函数内部打开了一个新的作用域,从而改善了程序流的结构。让我们看看他们做了什么,以及他们如何帮助我们写出更好的代码。
注意
顺便说一句,如果你需要一个助记符来记住它们,读读“让我们也用 APPLY 运行”
应用功能
让我们看看这些作用域函数中的第一个,apply
。你可以把它挂在任何物体上,比如
object.apply {
...
}
这看起来并不太冒险,但是神奇的是在apply
的花括号内的对象实例发生了什么:它被传输到this
。另外,apply
自动返回对象实例。因此,如果我们写this.someProperty
或someProperty
,或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
}
注意
因为propertyName
和functionName()
针对的是this
实例,所以我们也可以说this
代表了这种简单属性和函数访问的接收者。没有作用域函数,this
指的是周围的类实例或单例对象。随着this
在apply{ ... }
中被重新定义,.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
。这适用于任何对象。
版权属于:月萌API www.moonapi.com,转载请注明出处