Kotlin Cheat Sheet: 带值的枚举类和转换
1. 为枚举常量添加额外信息
在 Kotlin 中,可以通过枚举类来实现类型安全的枚举,如:
[code lang=”kotlin”]enum class Size {
SMALL, MEDIUM, LARGE;
}[/code]
每一个枚举常量都是一个对象,是枚举类的一个实例,可以为枚举类添加属性并进行初始化,这样就可以得到带有属性的枚举常量:
[code lang=”kotlin”]enum class Size(val sizeNumber: Int) {
SMALL(0), MEDIUM(1), LARGE(2);
}[/code]
上面的代码在 Size
的主构造器中声明了 Int
型只读属性 sizeNumber
,并在声明枚举常量时进行了初始化,SMALL
的 sizeNumber
为 0
,MEDIUM
的 sizeNumber
为 1
,LARGE
的 sizeNumber
为 2
。
为枚举常量添加只读属性,可以让枚举常量与特定的值进行绑定,为枚举常量加入额外的信息,如第二排序优先级(枚举类本身有一个 ordinal
属性表示排序顺序,为定义顺序)、接口值定义等。
2. 从枚举常量名称到枚举常量
Kotlin 提供了如下两个方法,分别用于从枚举名称转换为枚举常量,以及获取包含指定枚举类的所有枚举常量的数组:
[code lang=”kotlin”]EnumClass.valueOf(value: String): EnumClass
EnumClass.values(): Array<EnumClass>[/code]
使用方法如:
[code lang=”kotlin”]println(Size.valueOf(“SMALL”) == Size.SMALL) // true
println(Arrays.toString(Size.values()) ) // [SMALL, MEDIUM, LARGE]
Size.valueOf(“EXTRA_LARGE”) // throws IllegalArgumentException[/code]
注意使用 EnumClass.valueOf(value: String)
转换枚举类型时,如果不存在名称为 value
的枚举常量,则会抛出 IllegalArgumentException 异常。
3. 从自定义属性到枚举常量
如果要根据自定义属性来将一个值转换为枚举类型,则需要手动实现。要根据自定义属性查找枚举常量,首先需要建立一个从自定义属性到枚举常量的映射,以上面的枚举类 Size
为例,获取从 sizeNumber
到具体枚举常量的映射的方式如下:
[code lang=”kotlin”]val valueMap: Map<Int, Size> = Size.values().associateBy { it.sizeNumber }[/code]
上面的 associateBy() 是 Array
的一个扩展方法,将数组的元素以指定的 keySelector
为索引,生成一个 Map
。这里使用的 keySelector
为 it.sizeNumber
,即以 Size
的 sizeNumber
为键,以 Size
的枚举常量为值,生成一个 Map<Int, Size>
。
然后就可以轻松地根据 sizeNumber
索引对应的枚举常量了:
[code lang=”kotlin”]fun fromSizeNumber(sizeNumber: Int) = valueMap[sizeNumber][/code]
可以将以上方法实现在枚举类的伴生对象(Companion Object)里,重新定义枚举类 Size
如下:
[code lang=”kotlin”]enum class Size(val sizeNumber: Int) {
SMALL(0), MEDIUM(1), LARGE(2);
companion object {
private val valueMap: Map<Int, Size> = Size.values().associateBy { it.sizeNumber }
fun fromSizeNumber(sizeNumber: Int): Size? = valueMap[sizeNumber]
}
}[/code]
使用方法如:
[code lang=”kotlin”]println(Size.fromSizeNumber(1)) // MEDIUM
println(Size.fromSizeNumber(10)) // null[/code]
注意在上面的实现中,如果指定的 sizeNumber
不存在,返回值为 null
,这是由 Map
(valueMap
)的行为决定的。也可以修改 fromSizeNumber()
,当指定 sizeNumber
在 valueMap
中不存在时,抛出异常。
进一步地,可以将相关转换逻辑提取出来,得到一个更通用的形式:
[code lang=”kotlin”]abstract class EnumConverter<in T, R: Enum<R>>(
private val valueMap: Map<T, R>
) {
fun fromValue(value: T): R? = valueMap[value]
fun fromValue(value: T?, default: R): R = valueMap[value] ?: default
}
inline fun <T, reified R: Enum<R>> buildValueMap(keySelector: (R) -> T): Map<T, R> =
enumValues<R>().associateBy(keySelector)[/code]
重新定义枚举类 Size
如下:
[code lang=”kotlin”]enum class Size(val sizeNumber: Int) {
SMALL(0), MEDIUM(1), LARGE(2);
companion object : EnumConverter<Int, Size>(buildValueMap(Size::sizeNumber))
}[/code]
使用方法如:
[code lang=”kotlin”]println(Size.fromValue(0)) // SMALL
println(Size.fromValue(10, Size.LARGE)) // LARGE[/code]
buildValueMap(keySelector: (R) -> T)
方法通过在内联函数中使用具体化类型参数(Reified Type Parameters),获取指定枚举类的所有枚举常量,构造映射关系。EnumConverter
还提供了一个带默认值的 fromValue(value: T?, default: R)
方法,如果没有对应指定 value
的枚举常量,则返回默认值 default
。
如果将枚举的自定义属性抽象到接口中,则还可以有更紧凑的写法,完全隐藏 buildValueMap()
的细节,具体代码见这里。
4. 使用密封类(Sealed Class)构造高级枚举关系
Kotlin 还提供了密封类(Sealed Class),用于表现有限的类的层级关系,即限制一个值只能是某个特定集合中的一种。从这个角度来看,密封类与枚举非常相似,而二者的主要差异在于,枚举类的每个枚举常量只有一个实例,而一个密封类可以有多个实例。因此,密封类的实例可以保存更丰富的、可变的状态,本文不再展开。