在这个章节讨论的所有特征中,迭代在Kotlin
中可能是跟Java
最相似的。while
循环跟Java
中的那个一样。所以在这个章节的开头简要的提一下。for
循环只有一种形式,它等价于Java
中的for-each
循环。跟C#中的一样,写成for <item> in <elements>
。这种循环最常见的应用是遍历集合,正如Java
中的一样,我们将会探索它是如何覆盖其他场景的循环的。
2.4.1 while
循环
Kotlin
有while
和do-while
循环,同时,它们的语法跟Java
中对应的循环没什么不同:
while (condition) { // 1 当while条件为真时,执行主体代码
/*...*/
}
do {
/*...*/
} while (condition) // 2 第一次无条件的执行主体代码。在这之后,当条件为真时才执行。
Kotlin
并没有为这些简单的循环带来新的东西,所以我们不要在这消耗时间。让我们继续讨论for
循环的各种用法吧!
2.4.2 遍历数字:范围和数列
正如我们所提到的那样,在Kotlin
中并没有常规的Java for
循环让你初始化一个变量,在循环的每一步更新它的值,当值到达一个特定的边界时退出循环。为了替代这样一个最常用的循环,Kotlin
使用了ranges
的概念。
一个范围,在本质上就是两个值之间的一个间隔,通常是成员:一个起始值和一个结束值。你用..
操作符来写它:
val oneToTen = 1..10
注意:Kotlin
中的范围是闭合的或者说是包含的。这意味着第二个值也始终是范围的一部分。
对于整数范围你能做的最基本的是查看所有的值。如果你能遍历范围内的所有值,这样的一个范围叫做数列(progression)。
让我们用整数范围来玩一个Fizz-Buzz游戏。这是一个打发旅途时间和回想起你已经遗忘的除法技能的好方法。玩家随着报数增加轮流玩。玩家用fizz一词替换可以被3整除的数,用buzz一词替换可以被5整除的数字。如果一个数是3和5的乘积,你就说"FizzBuzz"。
下面的代码打印出从数字1到100的正确答案。注意你如何用不带参数的when
表达式来检查可能的情况:
fun fizzBuzz(i: Int) = when {
i % 15 == 0 -> "FizzBuzz " // 1 如果i能被15整除,返回FizzBuzz。就像在Java中,%是求模运算符
i % 3 == 0 -> "Fizz " // 2 如果i能被3整除,返回Fizz
i % 5 == 0 -> "Buzz " // 3 如果i能被5整除,返回Buzz
else -> "$i " // 4 否则返回这个数字的原始值
}
>>> for (i in 1..100) { // 5 遍历整数返回1..100
... print(fizzBuzz(i))
... }
}
1 2 Fizz 4 Buzz Fizz 7 …
假设你在开车一个小时之后对这些规则厌倦了,想一点一点的完成这个游戏。让我们开始从100向后计数,而且只包含偶数:
>>> for (i in 100 downTo 1 step 2) {
... print(fizzBuzz(i))
... }
Buzz 98 Fizz 94 92 FizzBuzz 88 …
现在,你在遍历一个有步进值(step)的数列。它允许你跳过一些数字。这个步进值也可以是负的。这种情况下数列是向后遍历而不是向前遍历。在这里案例中,100 downTo 1
是一个向后遍历的数列(步进值是-1)。之后step
变量把步进值改成了2同时保持遍历的方向不变(效果上是把步进值设为-2)。
正如我们之前提到的那样,..
语法始终产生一个包含终点的范围(..
右边的值)。在许多情况下,这让遍历不包含指定终点的半闭合范围更加方便。为了创建这样一个范围,(我们)使用util
函数。举个例子,for (x in 0 until size)
循环等价于for (x in 0..size-1)
,但无论如何它表达的观点更加清晰。在后续的“使用对:插入调用和解构声明(Working with paris: infix calls and destructuring declarations)”章节,你将会在这些例子中学到更多关于downTo, step
和until
的语法。你可以看到如何使用范围和数列来帮助你应对FizzBuzz游戏的高级规则。现在让我们看看其他使用for
循环的例子。
2.4.2 遍历映射集
我们已经提到过for .. in
循环最常见的场景是遍历集合。这一点跟Java
是完全一样的。因此我们不会说太多相关的东西。取而代之的是,让我们看看你能够如何遍历一个映射。
作为示例,我们将会看一些打印字符二进制值的小程序。你把这些二进制表示形式存储在一个映射里(仅仅是为了演示)。你创建一个映射,用一些字母的二进制值填满(这个映射集),然后打印这个映射集的内容:
val binaryReps = TreeMap<Char, String>() // 1 使用了TreeMap,因此键值是有序的
for (c in 'A'..'F') { // 2 使用字符范围,从A到F遍历字符
val binary = Integer.toBinaryString(c.toInt()) // 3 把ASCII编码转换成二进制
binaryReps[c] = binary // 4 以c为键把数值保存在映射集
}
for ((letter, binary) in binaryReps) { // 5 遍历一个映射集,吧映射的键跟值分配给两个变量
println("$letter = $binary")
}
..
语法创建的一个范围不仅对数字有效,对字符也是适用的。你在这里使用它来遍历从A
到包含F
的所有的字符。(上面的)示例展示了一个允许你对正在遍历的集合的元素进行拆箱(unpack)的for
循环。你把拆箱后的结果存储在两个不同的变量中:letter
接收键,binary
接收值。在后续的析构声明和循环(Destructuring declarations and loops)一章,你将会找出更多关于这个拆箱(操作)的语法。
这个示例中用到的另一个新颖的技巧是通过键来获取和更新映射集中的值的简写语法。你可以使用map[key]
来读取值并且通过map[key] = value
来设置值,而不是调用get()
和put()
。代码binaryReps[c] = binary
等价于Java
版中的binaryReps.put(c, binary)
。
它的输出跟下面的(示例)是类似的。我们把结果安排成两列而不是一列:
A = 1000001 D = 1000100
B = 1000010 E = 1000101
C = 1000011 F = 1000110
你可以在记录当前项(item)的索引的同时使用同样的拆箱语法来遍历集合。你不必创建一个单独的变量来保存索引和手动增加这个变量的值:
val list = arrayListOf("10", "11", "1001")
for ((index, element) in list.withIndex()) { // 通过索引遍历集合
println("$index: $element")
}
(上面的)代码输出了你所期待的(结果):
0: 10
1: 11
2: 1001
我们将会在下一章节深入探究withIndex
的去向。
你已经看到你可以如何使用in
关键字来遍历一个返回或者一个集合。你也可以使用in
来检查一个值是否属于(某个)范围或集合。
2.4.4 使用in
检查
你可以使用in
操作符来检查一个值是否在某个范围内,或者相反的,!in
(操作符)来检查一个值是否不再某个范围内。以下(演示了)你可以如何使用in
来检查一个字符是否在某个字符范围内:
fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z'
fun isNotDigit(c: Char) = c !in '0'..'9'
>>> println(isLetter('q'))
true
>>> println(isNotDigit('x'))
true
(用来)检查一个字符是否为字母的技术看起来很简单。掀开它的面纱,也没有什么花样:你依然是检查字符的编码是在第一个字母的编号与最后一个字母的标号之间的那个位置。但这个逻辑只是隐藏在了标准库中range
类的实现中:
c in 'a'..'z' // 1 转换为a c && c z
in
和!in
操作符在when
表达式中也是有效的:
fun recognize(c: Char) = when (c) {
in '0'..'9' -> "It's a digit!" // 1 判断值是否在0到9的范围内
in 'a'..'z', in 'A'..'Z' -> "It's a letter!" // 2 你可以合并多个范围
else -> "I don't know…"
}
>>> println(recognize('8'))
It's a digit!
范围并没有局限于字符。如果你有任何支持(通过实现java.lang.Comparabble
接口的)实例比较算法的类,你可以创建一个这种类型的范围对象。如果你有这样一个范围,你不能枚举范围内的所有对象。想象一下这样的场景,举个例子,你能够枚举"Java"和"Kotlin"之间的所有字符串吗?不,你做不到。但是你依然可以使用in
操作符来检查一个对象是否在这个范围内。
>>> println("Kotlin" in "Java".."Scala") // "Java".."Kotlin"和"Kotlin".."Scala"是一样的
true
注意字符串在这里是可以按字符顺序比较的,因为String
类就是这样实现Comparable
接口的。
in
检查对集合也是有效的:
>>> println("Kotlin" in setOf("Java", "Scala")) // 这个集合并没有包含"Kotlin"字符串。在这个章节的后续部分,你将会看到如何对你的数据类型使用范围和数列以及如何为那些对象使用in检查。
false
在这一章,我们想看到(这样)一组或者多组Java声明:处理异常的声明。