在这个章节里,我们将讨论when
结构。它可以被理解为Java
中switch
的替代,但事实上,它更加强大而且常用。一路上,我们将会给你一个声明枚举的例子并讨论智能类型转换的概念。
2.3.1 声明枚举类
让我们以为这本严肃的书添加一些富有想象的图片并看看颜色枚举值为开始吧:
enum class Color {
RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}
在Kotlin
中使用的关键词比Java
对应的要多,这是很少见的一种情况:enum class
对应Java
中的enum
。在Kotlin
中,enum
又叫做软关键词(soft keyword):当它出现在class
之前时,它就有了特殊的含义。但是你可以在其他地方把它当做常规名字来使用。另一方面,class
依然是一个关键词。你依然需要把变量命名为(class
关键字以外的名字)clazz
或aClass
。
像Java
那样,枚举类型并不是一个值的列表:你可以在枚举类中声明属性和方法。以下展示了它是如何工作的:
enum class Color(
val r: Int, val g: Int, val b: Int // 1 声明枚举常量的属性
) {
RED(255, 0, 0), ORANGE(255, 265, 0), // 2 当每个常量被创建时指定属性值
YELLOW(255, 255, 0), GREEN(0, 255, 0), BLUE(0, 0, 255),
INDIGO(75, 0, 130), VIOLET(238, 130, 238); // 3 分号(;)在这里是必须的
fun rgb() = (r * 256 + g) * 256 + b // 4 在枚举类中定义了一个方法
}
>>> println(Color.BLUE.rgb())
255
如你所见,枚举常量使用了跟你之前看到的在常规类中声明构造函数和属性的语法是一样的。当你声明每一个枚举常量时,你需要为常量提供属性值。注意,这个示例中展示了Kotlin
语法中唯一一处需要你使用分号的地方:如果你在枚举类中定义了任何方法,(请使用)分号将枚举常量列表从函数定义中分隔开来。现在,让我们来看看一些酷的方式来处理代码中的枚举常量。
2.3.2 使用when
来处理枚举类
你还记得小孩子是如何使用助记词来记忆彩虹的颜色的吗?这一个例子:"Richard Of York Gave Battle In Vain!"。想象一下你需要一个为每种颜色给出一个助记词的函数(但是你不想把这些信息存储在枚举列表中)。在Java
中,你可以为此使用一个switch
声明。在Kotlin
中对应的(语法)构造是when
。
跟if
关键词类似,when
是一个返回值的表达式,因此你可以写一个有表达式主体的函数来直接返回when
表达式。当我们在这一章的开头讨论函数时,我们允诺了会有一个带有多个表达式主体的多行函数。这就是那个的一个例子了:
fun getMnmonic(color: Color) = // 1 直接返回一个when表达式
when (color) { // 2 如果颜色等于枚举常量,返回对应的字符串
Color.RED -> "Richard"
Color.ORANGE -> "Of"
Color.YELLOW -> "York"
Color.GREEN -> "Grave"
Color.BLUE -> "Battle"
Color.INDIGO -> "In"
Color.VIOLET -> "Vain"
}
>>> println(getMnemonic(Color.BLUE))
Battle
代码找出传递给color
值对应的分支。不像Java
,你不需要再每一个分支里写break
语句(在Java
代码中,一个缺失的break
经常导致bug)。如果匹配成功,只有对应的分支会被执行。
你也可以在一个分支中合并多个值,如果你用逗号将它们分隔开:
fun getWarmth(color: Color) = when(color) {
Color.RED, Color.ORANGGE, Color.YELLOW -> "warm"
Color.GREEN -> "neutral"
Color.BLUE, Color.INDIGO, Color.VIOLET -> "cold"
}
>>> println(getWarmth(Color.ORANGE))
warm
这些例子使用了枚举常量的完整名称,指定了Color
枚举类名。你可以通过导入常量值来简化代码:
import ch02.colors.Color // 1 导入在另一个包里声明的Color类
import ch02.colors.Color.* // 2 显式地导入枚举常量,然后通过名字来使用它们
fun getWarmth(color: Color) = when(color) {
RED, ORANGE, YELLOW -> "warm" // 3 通过名字来使用常量
GREEN -> "neutral"
BLUE, INDIGO, VIOLET -> "cold"
}
在后续的例子中,我们将会使用简短的枚举名字,但是为了简单起见会忽略显式导入。
2.3.3 对任意对象使用when
Kotlin
中的when
构造比Java
中的switch
更为强大。跟要求你使用常量(枚举常量,字符串或者数字字面量)作为分支条件的switch
不同,when
允许任意的对象。让我们写一个函数来混合两种颜色,如果它们能够在这个小调色板上能够被混合。你没有太多的选项,同时你也可以便捷的全部枚举它们:
fun mix(c1: Color, c2: Color) =
when (setOf(c1, c2)) { // 1 一个when表达式的参数可以是任意的对象。它检查分支的等价性。
setOf(RED, YELLOW) -> ORANGE // 2 枚举颜色键值对可以是混合的
setOf(YELLOW, BLUE) -> GREEN
setOf(BLUE, VIOLET) -> INDIGO
else -> throw Exception("Dirty color") // 3 如果没有一个分支被匹配,将执行该语句
}
>>> println(mix(BLUE, YELLOW))
GREEN
如果颜色c1和c2是RED
和YELLOW
(或者相反),两者混合的结果是ORANGE
,以此类推。为了实现这个目的集合比较,你使用了。Kotlin
标准库内置了一个创建包含指定对象作为参数的一个setOf
函数。一个set
是一个元素顺序无关的集合。如果两个集合包含相同的元素,那么它们是等价的。因此,如果集合setOf(c1, c2)
和setOf(RED, YELLOW)
是等价的,这意味着无论c1是RED
同时c2是YELLOW
,反之亦然。这恰恰就是你想要检查的。
when
表达式匹配它的参数直到分支条件是符合,而不是按顺序匹配所有的分支。所以,setOf(c1, c2)
检查等价性:首先是setOf(RED, YELLOW)
,然后才是其他颜色的集合,一个接着一个。如果没有其他分支条件符合,else
分支就会被执行。
允许使用任意的表达式作为一个when
条件让你能够在许多场合下编写精简和出色的代码。在这个例子中,判断条件是一个等价性检查,接下来你将会看到条件是如何变为任意的布尔表达式的。
2.3.4 使用不带参数的when
你可能已经注意到前一个例子(的实现)有些效率低下。你每次调用这个函数,它都会创建多个仅仅是用来检查给出的两个颜色是否匹配另外两个颜色的Set
实例。一般情况下这不会有问题,但是如果这个函数是经常被调用的,通过其他方式来重写这段代码以避免创建内存垃圾是值得的。你可以通过使用不带参数的when
语句来达到这个目的。这样代码可读性会降低,但这往往是你为了达到更好性能而必须付出的代价:
fun mixOptimized(c1: Color c2: Color) =
when { // 不带参数的when
(c1 == RED && c2 == YELLOW) ||
(c1 == YELLOW && c2 == RED) ->
ORANGE
(c1 == YELLOW && c2 == BLUE) ||
(c1 == BLUE && c2 == YELLOW) ->
GREEN
(c1 == BLUE && c2 == VIOLET) ||
(c1 == VIOLET && c2 == BLUE) ->
INDIGO
else -> throw Exception("Dirty color")
}
>>> println(mixOptimized(BLUE, YELLOW))
GREEN
如果when
表达式没有使用任何参数,分支条件将会是任意的布尔表达式。mixOptimized
函数跟之前的mix
函数做的是同样一件事。它的好处是不会创建额外的对象,但(相应的)代价是变得难以阅读。
让我们继续来看when
结构在智能类型转换(smart casts)中发挥作用的例子。
2.3.5 智能类型转换:合并类型检查和转换
作为这个章节的一个例子,你将会写一个计算像(1 + 2) + 4这样的简单数学表达式的函数。这个表达式将会包含一种类型操作:求两个数的和。其他的算术操作(减法、乘法、除法)也能够用相似的方式来实现。你也可以把它作为一个练习。
首先,你如何编码这个表达式?你把它们存储在一个类似于树的结构中,其中的每一个节点是一个和(Sum
)或者是一个数(Num
)。Num
一直是一个叶子节点,尽管Sum
节点有两个孩子:sum
操作的参数。接下来的代码片段展示了一个用来编码表达式的简单的类结构:一个叫做Expr
的接口、实现这个接口的两个类Num
和Sum
。注意,Expr
接口并没有声明任何方法。它被用做一个提供不同种类的表达式的公共类型接口记号。为了标记一个类是实现了某个接口的,你(应该)在接口名字的后面使用一个冒号(:):
interface Expr
class Num(val value: Int) : Expr // 1 带有一个属性、值而且实现了Expr接口的简单的值对象类
class Sum(val left: Expr, val right: Expr) : Expr // 2 求和操作的参数可以是任意的Expr:Num对象或者其他的Sum对象
Sum
存储了Expr
类型的left
和right
的引用。在这个小案例中,它们可以是Num
或者是Sum
。为了存储之前提到的表达式(1 + 2) + 4,你(应该)创建一个Sum(Sum(Num(1), Num(2)), Num(4))
。图2.4展示了它的树形表示。
图2.4 Sum(Sum(Num(1), Num(2)), Num(4))
表达式的一个展现
让我们看看表达式是如何求值的。计算示例中的表达式应该返回7:
>>> eval(Sum(Sum(Num(1), Num(2)), Num(4)))
7
Expr
接口有两个实现,所以,为了计算一个表达式的值你必须尝试(以下)两种选项:
- 如果一个表达式是一个值,你必须返回对应的值
- 如果它是一个和,你必须由左到右计算表达式并返回它们的和
首先,我们将会着眼于用Java
的常规方式来写的函数,然后我们会用Kotlin
的方式来重构这份代码。在Java
中,你可能已经实用一系列的if
语句来检查选项,因此,让我们在Kotlin
中使用同样的方法:
fun eval(e: Expr): Int {
if (e is Num) {
val n = e as Num // 1 显式的Num类型转换是多余的
return n.value
}
if (e is Sum) {
return eval(e.right) + eval(e.left) // 2 变量e是智能类型转换
}
throw IllegalArgumentException("Unknown expression")
}
>>> println(eval(Sum(Sum(Num(1), Num(2)), Num(4))))
7
在Kotlin
中,你通过is
检查一个变量是否为一个指定的类型。如果你有C#的编程经验, 你应该对这个标记很熟悉。is
检查跟Java
中的instanceof
很相似。但是在Java
中,如果你已经检查一个变量为某个指定的类型,而且必须以指定的类型访问那个成员,那么你得在instanceof
检查后面添加一个显式的类型转换。当初始变量使用超过一次,你往往会将类型转换后的结果保存到另一个变量中。在Kotlin
中,编译器为你做了这份工作。如果你检查了变量的特定类型,后续你不需要执行类型转换。你可以把它当做你检查的目标类型来使用。实际上,编译器为你执行了类型转换,我们把这叫做智能类型转换(smart cast)。
在eval
函数中,你检查变量e
是否为Num
类型之后,编译器会把这个变量当做一个Num
的变量。之后你可以访问Num
的value
属性而不需要显式的类型转换:e.value
。Sum
的right
和left
属性也是同样的道理:你只需要在相应的上下文中写e.right
和e.left
。在IDE中,这些智能类型转换值会通过一个背景颜色来强调,如图2.5所示,因此很容易理解这个值是之前检查的那个。
图2.5 IDE用一个背景颜色对智能类型转换做了高亮处理
当且仅当一个变量在is
检查之后不再改变时,智能类型转换才会起作用。当你对一个带有属性的类使用智能类型转换时,正如(上面的)这个例子,属性必须是一个val
(不可变类型),同时它不能有自定义的访问器。否则,它不能验证每一个属性的访问是否会返回同样的值。
一个指定类型的显式转换通过as
关键词来表达的:
val n = e as Num
现在让我们来看看如何把一个eval
函数重构成一个更符合Kotlin
风格的函数。
2.3.6 重构:用when
替换if
Kotlin
中的if
跟Java
中的if
有何不同?(事实上,)你已经见过(两者间的)差异了。在这一章的开头,你看到了if
表达式用在Java中用三元操作符的上下文中:if (a > b)
,就像Java中的a > b ? a : b
。Kotlin中并没有三元操作符,因为,与Java不同,if
表达式返回一个值。这意味着你可以重写eval
函数来使用表达式主体语法、移除return
语句和闭合的大括号,相反的,使用if
表达式作为函数的主体:
fun eval(e: Expr): Int =
if (e is Num) {
e.value
} else if (e is Sum) {
eval(e.right) + eval(e.left)
} else {
throw IllegalArgumentException("Unknown expression")
}
>>> println(eval(Sum(Num(1), Num(2))))
3
如果if
分支里只有一个表达式,闭合的括号是可选的。如果if
分支是一个代码块,最后的一句表达式作为结果返回。
让我们用when
重写这份代码来进一步为它进行润色:
fun eval(e: Expr): Int =
when (e) {
is Num -> // 1 用于检查参数类型的when分支
e.value // 2 这里使用了智能类型转换
is Sum -> // 1 用于检查参数类型的when分支
eval(e.right) + eval(e.left) // 2 这里使用了智能类型转换
else ->
throw IllegalArgumentException("Unknown expression")
}
when
表达式没有强制要求检查值的等价性。这是你早前看到的。在这里,你使用了一个不同形式的when
分支,允许你检查when
参数值的类型。正如先前的if
例子,类型检查运用了一个智能类型转换,因此你可以访问Num
和Sum
的成员而无需额外的类型转换。
比较以上两个Kotlin
版的eval
函数,然后思考一下你怎样才能在你的代码中运用when
替换一连串的if
语句。当分支逻辑复杂时,你可以使用表达式块作为分支主体。让我们看看这是如何运作的。
2.3.7 代码块作为if
和when
的分支
if
和when
都能够使用块作为分支。在这个例子中,代码块的最后一个表达式是结果。如果你想添加一些记录到示例函数中,你可以在返回最后一个值之前做这件事:
fun evalWithLogging(e: Expr): Int =
when (e) {
is Num -> {
println("num: ${e.value}")
e.value // 1 这是代码块中的最后一个表达式。如果e是Num类型的它将会被返回。
}
is Sum -> {
val left = evalWithLogging(e.left)
val right = evalWithLogging(e.right)
println("sum: $left + $right")
left + right // 2 如果e是Sum类型的, 这个表达式将会被返回
}
else -> throw IllegalArgumentException("Unknown expression")
}
现在你能够看到通过evalWithLogging函数打印的日志和接着的计算顺序:
>>> println(evalWithLogging(Sum(Sum(Num(1), Num(2)), Num(4))))
num: 1
num: 2
sum: 1 + 2
num: 4
sum: 3 + 4
7
“代码中的最后一个表达式就是结果”这个规则在所有可以使用代码并需要一个(返回)结果的地方都是有效的。正如你将在这一章末尾看到的那样,同样的规则对try代码块catch
从句有效。第5章讨论了lambda表达式中它的应用。但是正如我们已经在2.2.1章节中已经提到的那样,这个规则对于常规的函数是无效的。一个函数可以有一个不能为代码块的表达式主体,或者一个内部有显式return
语句的块主体。
你已经了解了如何用Kotlin
的方式在众多事物中选择你想要的的东西。现在,是时候来看看你如何进行遍历的。