5.8.9.2 枚举类
原文链接: https://kotlinlang.org/docs/enum-classes.html
5.8.9.2 枚举类
枚举类表示一组固定的可能取值。当某个值只能是若干预定义选项之一时(例如可用的状态或模式),就使用枚举类。
枚举类中的每个值称为一个枚举常量。枚举常量的行为就像枚举类类型的单例对象,因此它们可以有属性、函数和自定义行为。
当所有可能取值都是事先已知、并且结构相同时,枚举类是最合适的选择。如果你需要为每种情况保存不同的数据或采用不同的结构,请使用密封类或密封接口。
声明枚举类
要创建枚举类,请使用 enum 关键字,并按通常的类语法在花括号中写类体。在类体中列出枚举常量,用逗号分隔:
1
2
3
| enum class Direction {
NORTH, SOUTH, WEST, EAST
}
|
在这个例子中,Direction 是枚举类,NORTH、SOUTH、WEST 和 EAST 是枚举常量。
按照约定,枚举常量通常使用大写书写,因为它们表示常量值。
你可以通过枚举类名后跟常量名来访问枚举常量:
1
2
3
4
5
6
7
8
9
10
11
| enum class Direction {
NORTH, SOUTH, WEST, EAST
}
fun main() {
// `Direction.NORTH` 是 `Direction` 类型的枚举常量。
val direction: Direction = Direction.NORTH
println(direction)
// NORTH
}
|
Kotlin 中每个枚举类都继承自 Enum<T> 基类,其中 T 就是该枚举类本身。例如,Direction 枚举类继承自 Enum<Direction>。正因如此,枚举常量才拥有诸如 name 和 ordinal 这样的内置属性。
使用枚举常量
由于枚举常量是值,你可以把它们赋给变量、打印它们、传给函数、比较它们,并在 when 表达式中使用它们。
声明枚举常量
要声明枚举常量,请先在枚举类构造函数中定义属性,然后在圆括号中为每个枚举常量传入值。与其他一些语言不同,Kotlin 不使用赋值语法,例如 RED = "#FF0000"。
枚举常量可以带有任何类型的关联值。字符串和数字是常见例子,你也可以使用其他类型,例如 Boolean、另一个枚举类或自定义类。
来看 Color 枚举类,它为每种颜色保存一个十六进制颜色代码:
1
2
3
4
5
| enum class Color(val hex: String) {
RED("#FF0000"),
GREEN("#00FF00"),
BLUE("#0000FF")
}
|
传给每个枚举常量的值必须与构造函数参数类型匹配。这里 hex 是 Color 枚举类的字符串属性,每个枚举常量都为该属性传入自己的字符串值。
你也可以把数值关联到枚举常量上。例如,在构造函数中声明 Int 类型,并为每个枚举常量提供一个 Int 值:
1
2
3
4
5
| enum class Priority(val level: Int) {
LOW(0),
MEDIUM(1),
HIGH(2)
}
|
访问枚举常量及其属性
你可以通过枚举类名访问枚举常量。要访问与枚举常量关联的属性,请使用点号记法,例如 color.hex 或 Color.GREEN.hex:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| enum class Color(val hex: String) {
RED("#FF0000"),
GREEN("#00FF00"),
BLUE("#0000FF")
}
fun main() {
val color: Color = Color.RED
println(color)
// RED
println(color.hex)
// #FF0000
println(Color.GREEN.hex)
// #00FF00
}
|
这里 Color.RED 是 Color 类型的枚举常量。color 变量保存了这个枚举常量。
除了你自己定义的属性之外,每个枚举常量还有内置的 name 和 ordinal 属性,分别用于获取它的名称和在枚举类声明中的位置(从 0 开始):
1
2
3
4
5
6
7
8
| enum class RGB { RED, GREEN, BLUE }
fun main() {
println(RGB.RED.name)
// RED
println(RGB.RED.ordinal)
// 0
}
|
把枚举常量传给函数
由于枚举常量是值,你可以把它们传给函数。这样函数就只接受枚举类中定义的这组固定选项,从而保证代码类型安全:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| enum class Color(val hex: String) {
RED("#FF0000"),
GREEN("#00FF00"),
BLUE("#0000FF")
}
fun printColor(color: Color) {
println("Color: $color")
println("Hex code: ${color.hex}")
}
fun main() {
printColor(Color.BLUE)
// Color: BLUE
// Hex code: #0000FF
}
|
这里 printColor() 函数接受 Color 类型的值,因此你可以把任何 Color 枚举常量传给它。
虽然枚举常量的行为像单例对象,但编译器把它们视为枚举类类型的值。你可以把枚举类名本身用作类型,但不能把枚举常量用作枚举类型:
1
2
3
4
5
6
7
8
9
10
11
12
| enum class Color {
RED, GREEN, BLUE
}
fun printColor(color: Color) {
println(color)
}
fun printRed(color: Color.RED) {
println(color)
// 错误:枚举项不能用作类型
}
|
在 when 表达式中使用枚举常量
当你想分别处理每个常量时,枚举类与 when 表达式配合得最好:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| enum class Color(val hex: String) {
RED("#FF0000"),
GREEN("#00FF00"),
BLUE("#0000FF")
}
fun describeColor(color: Color): String {
return when (color) {
Color.RED -> "Red is a warm color"
Color.GREEN -> "Green is a natural color"
Color.BLUE -> "Blue is a cool color"
}
}
fun main() {
println(describeColor(Color.RED))
// Red is a warm color
}
|
当你在 when 表达式中使用了所有枚举常量时,就不需要 else 分支。
提示: 要减少处理枚举项时的重复,可以试试上下文敏感解析(目前为预览功能)。当预期类型已知时(例如在 when 表达式中或赋值给有类型的变量时),该功能允许你省略枚举类名。更多信息请参阅上下文敏感解析预览或相关的 KEEP 提案。
查找枚举常量
有时你需要从字符串、索引或某个关联值获取枚举常量。Kotlin 提供了内置 API,可以按名称、位置或自定义值查找常量。
例如,考虑一个枚举类,其中每种颜色都有一个关联的 RGB 值。要按名称查找枚举常量,请使用 valueOf() 函数:
1
2
3
4
5
6
7
8
9
10
11
12
| enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}
fun main() {
val color = Color.valueOf("RED")
println(color)
// RED
}
|
传给 valueOf() 的名称必须与枚举常量名称完全一致。如果不存在指定名称的枚举常量,valueOf() 会抛出 IllegalArgumentException。
要按枚举声明中的位置查找枚举常量,请对枚举的 entries 属性使用 getOrNull() 函数:
1
2
3
4
5
6
7
8
9
10
11
12
| enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}
fun main() {
val color = Color.entries.getOrNull(0)
println(color)
// RED
}
|
枚举位置从 0 开始。在这个例子中,RED 的位置是 0,GREEN 是 1,BLUE 是 2。
当你有一个表示枚举常量位置的整数(例如来自文件或用户输入)时,这很有用。与其他一些语言不同,Kotlin 不允许把 Int 直接转换为枚举常量。请改用该整数作为索引,通过 entries.getOrNull(index) 查找常量。
如果该整数表示的值即使重新排列枚举常量也应保持稳定,请定义一个显式的数值属性(例如 rgb 或 code),并查找具有匹配值的常量。
由于 entries 是特殊的 List,你可以对它使用标准的集合 API。例如,要按关联值查找枚举常量,可以用 first() 遍历 entries:
1
2
3
4
5
6
7
8
9
10
11
12
| enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}
fun main() {
val color = Color.entries.first { it.rgb == 0xFF0000 }
println(color)
// RED
}
|
如果没有找到匹配的常量,first() 函数会抛出 NoSuchElementException。若要改为返回 null,请使用 firstOrNull()。
要获取枚举常量的数量,请使用 size 属性。例如:
1
2
3
4
5
6
7
8
9
10
| enum class RGB { RED, GREEN, BLUE }
fun main() {
println(RGB.entries)
// [RED, GREEN, BLUE]
println(RGB.entries.size)
// 3
println("The first color is: ${RGB.valueOf("RED")}")
// "The first color is: RED"
}
|
如果你经常需要按名称、位置或关联值查找枚举常量,可以在伴生对象中添加辅助函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF);
companion object {
fun fromName(name: String): Color? =
entries.find { it.name == name }
fun fromPosition(position: Int): Color? =
entries.getOrNull(position)
fun fromRgb(rgb: Int): Color? =
entries.find { it.rgb == rgb }
}
}
fun main() {
println(Color.fromName("RED"))
// RED
println(Color.fromPosition(1))
// GREEN
println(Color.fromRgb(0x0000FF))
// BLUE
println(Color.fromRgb(0xABCDEF))
// null
}
|
当你希望查找是安全的、失败时返回 null 而不是抛出异常时,伴生对象辅助函数很有用。
上面用到的查找 API(例如 entries 和 valueOf())是合成成员的例子。在这里,合成意味着 Kotlin 自动提供了这些成员,尽管你并没有自己声明它们。正因如此,每个枚举类都能用 entries 属性列出其常量,并用 valueOf() 函数按名称获取常量,而无需编写额外代码。
你可以使用 enumEntries<T>() 和 enumValueOf<T>() 这类泛型辅助函数访问枚举类中的常量。这些函数使用具体化类型参数。这类参数让实际的枚举类型在泛型内联函数内部保持可用,因此辅助函数可以直接使用枚举类型 T:
| 函数 | 说明 |
| enumEntries<T>() | (推荐)返回枚举类型 T 的所有枚举项。每次调用返回同一个列表。 |
| enumValues<T>() | 返回包含枚举类型 T 所有枚举项的数组。每次调用 enumValues<T>() 都会新建一个数组。 |
| enumValueOf<T>() | 按名称返回单个枚举项,如果没有匹配的枚举项则抛出 IllegalArgumentException。 |
例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| import kotlin.enums.enumEntries
enum class RGB { RED, GREEN, BLUE }
inline fun <reified T : Enum<T>> printAllValues() {
println(enumEntries<T>().joinToString { it.name })
}
inline fun <reified T : Enum<T>> findByName(name: String): T = enumValueOf<T>(name)
fun main() {
printAllValues<RGB>()
// RED, GREEN, BLUE
println(findByName<RGB>("GREEN"))
// GREEN
}
|
关于内联函数和具体化类型参数的更多信息,请参阅内联函数。
比较与排序枚举常量
使用 == 结构相等性运算符比较枚举常量:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| enum class Color(val hex: String) {
RED("#FF0000"),
GREEN("#00FF00"),
BLUE("#0000FF")
}
fun main() {
val color = Color.RED
println(color == Color.RED)
// true
println(color == Color.BLUE)
// false
}
|
由于每个枚举常量的行为都像单例对象,比较枚举常量实际上是在检查两个值是否引用同一个常量。
所有枚举类默认都实现 Comparable 接口,因此你可以比较和排序枚举常量。常量按其声明位置(即 ordinal 值)排序,也就是说最先声明的常量被认为最小:
1
2
3
4
5
6
7
8
9
10
| enum class Priority {
LOW, MEDIUM, HIGH
}
fun main() {
println(Priority.LOW < Priority.HIGH)
// true
println(Priority.HIGH > Priority.MEDIUM)
// true
}
|
排序遵循相同的声明顺序。例如,entries.sorted() 会按声明顺序返回常量,与它们的名称无关:
1
2
3
4
5
6
7
8
| enum class Priority {
HIGH, LOW, MEDIUM
}
fun main() {
println(Priority.entries.sorted())
// [HIGH, LOW, MEDIUM]
}
|
Enum<T> 类提供了 compareTo()、equals() 和 hashCode() 函数,你无法像普通类那样重写它们来自定义行为。比较始终遵循声明顺序。
如果你需要不同的顺序,不要依赖声明顺序,而应定义一个显式属性并按其排序。例如,按亮度对颜色排序:
1
2
3
4
5
6
7
8
9
10
| enum class Color(val brightness: Int) {
RED(1),
GREEN(3),
BLUE(2)
}
fun main() {
println(Color.entries.sortedBy { it.brightness })
// [RED, BLUE, GREEN]
}
|
更多信息请参阅排序。
为枚举类添加函数
与属性一样,枚举类也可以有函数。你可以添加由所有枚举常量共享的函数,把它们与属性结合使用,或定义运算符函数。
添加所有常量共有的函数
要添加每个枚举常量都共享的行为,请在枚举类体中定义函数。如果枚举类定义了任何成员,请用分号把常量定义与成员定义分隔开:
1
2
3
4
5
6
7
8
9
10
11
12
| enum class Direction {
NORTH, SOUTH, WEST, EAST;
fun isVertical(): Boolean = this == NORTH || this == SOUTH
}
fun main() {
println(Direction.NORTH.isVertical())
// true
println(Direction.EAST.isVertical())
// false
}
|
每个枚举常量都可以调用该共享函数。在函数内部,this 指调用它的那个枚举常量。
你可以把构造函数属性与函数结合,从而为每个常量关联数据,并添加使用这些数据的行为:
1
2
3
4
5
6
7
8
9
10
11
12
| enum class Color(val hex: String) {
RED("#FF0000"),
GREEN("#00FF00"),
BLUE("#0000FF");
fun describe(): String = "$name has hex code $hex"
}
fun main() {
println(Color.RED.describe())
// RED has hex code #FF0000
}
|
这里每个常量都保存自己的 hex 值,而共享的 describe() 函数同时使用内置的 name 属性和 hex 属性。
添加运算符函数
枚举类也可以定义运算符函数,因此可以把枚举常量与运算符一起使用。例如,定义 not() 运算符函数,用 ! 运算符返回相反的方向:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| enum class Direction {
NORTH, SOUTH, WEST, EAST;
operator fun not(): Direction = when (this) {
NORTH -> SOUTH
SOUTH -> NORTH
WEST -> EAST
EAST -> WEST
}
}
fun main() {
println(!Direction.NORTH)
// SOUTH
}
|
使用匿名类
枚举常量可以声明自己的匿名类,并在其中定义相应的函数,以及重写基类函数。使用匿名类时,你把类体直接写在枚举常量名之后,Kotlin 会推断出枚举类作为超类型。
当你在枚举类中声明抽象函数并要求每个常量提供自己的实现时,这很有用。每个常量都在自己的匿名类中重写该抽象函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| enum class ProtocolState {
WAITING {
override fun signal() = TALKING
},
TALKING {
override fun signal() = WAITING
};
abstract fun signal(): ProtocolState
}
fun main() {
var state = ProtocolState.WAITING
println(state)
// WAITING
state = state.signal()
println(state)
// TALKING
}
|
这里每个常量以不同方式实现抽象函数 signal(),因此调用 signal() 会根据常量返回不同的下一个状态。
虽然枚举常量的行为像单例对象,但枚举常量的类型就是枚举类本身,而不是它自己的匿名类。这就是为什么你无法访问匿名类体中声明的成员:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| enum class ProtocolState {
WAITING {
val waitingMessage = "Waiting for a signal"
override fun signal() = TALKING
},
TALKING {
override fun signal() = WAITING
};
abstract fun signal(): ProtocolState
}
fun main() {
println(ProtocolState.WAITING.waitingMessage)
// 错误:未解析的引用 'waitingMessage'
}
|
要为每个常量公开数据或行为,请在枚举类体中声明它们;当每个常量需要自己的实现时,就使用抽象成员。
在枚举类中实现接口
枚举类可以实现接口,但不能继承类。你可以为所有枚举常量提供接口成员的公共实现,也可以让每个常量在匿名类中提供自己的实现。
要实现接口,请把它加到枚举类声明中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| import java.util.function.BinaryOperator
import java.util.function.IntBinaryOperator
enum class IntArithmetics : BinaryOperator<Int>, IntBinaryOperator {
PLUS {
override fun apply(t: Int, u: Int): Int = t + u
},
TIMES {
override fun apply(t: Int, u: Int): Int = t * u
};
override fun applyAsInt(t: Int, u: Int) = apply(t, u)
}
fun main() {
val a = 13
val b = 31
for (f in IntArithmetics.entries) {
println("$f($a, $b) = ${f.apply(a, b)}")
}
}
|
在这个例子中,IntArithmetics 枚举类在枚举类声明中实现了两个接口:BinaryOperator<Int> 和 IntBinaryOperator。每个常量都可以在自己的匿名类体中重写接口成员,就像 PLUS 和 TIMES 对 apply() 所做的那样,而 applyAsInt() 则提供了所有常量共享的实现。