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() 则提供了所有常量共享的实现。