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,并在声明枚举常量时进行了初始化,SMALLsizeNumber0MEDIUMsizeNumber1LARGEsizeNumber2

  为枚举常量添加只读属性,可以让枚举常量与特定的值进行绑定,为枚举常量加入额外的信息,如第二排序优先级(枚举类本身有一个 ordinal 属性表示排序顺序,为定义顺序)、接口值定义等。

2. 从枚举常量名称到枚举常量

  Kotlin 提供了如下两个方法,分别用于从枚举名称转换为枚举常量,以及获取包含指定枚举类的所有枚举常量的数组:

[code lang=”kotlin”]EnumClass.valueOf(value: String): EnumClass
EnumClass.values(): Array&ltEnumClass&gt[/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&ltInt, Size&gt = Size.values().associateBy { it.sizeNumber }[/code]

  上面的 associateBy()Array 的一个扩展方法,将数组的元素以指定的 keySelector 为索引,生成一个 Map。这里使用的 keySelectorit.sizeNumber,即以 SizesizeNumber 为键,以 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&ltInt, Size&gt = 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,这是由 MapvalueMap)的行为决定的。也可以修改 fromSizeNumber(),当指定 sizeNumbervalueMap 中不存在时,抛出异常。

  进一步地,可以将相关转换逻辑提取出来,得到一个更通用的形式:

[code lang=”kotlin”]abstract class EnumConverter&ltin T, R: Enum&ltR&gt&gt(
private val valueMap: Map&ltT, R&gt
) {
fun fromValue(value: T): R? = valueMap[value]
fun fromValue(value: T?, default: R): R = valueMap[value] ?: default
}

inline fun &ltT, reified R: Enum&ltR&gt&gt buildValueMap(keySelector: (R) -&gt T): Map&ltT, R&gt =
enumValues&ltR&gt().associateBy(keySelector)[/code]

  重新定义枚举类 Size 如下:

[code lang=”kotlin”]enum class Size(val sizeNumber: Int) {
SMALL(0), MEDIUM(1), LARGE(2);
companion object : EnumConverter&ltInt, Size&gt(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),用于表现有限的类的层级关系,即限制一个值只能是某个特定集合中的一种。从这个角度来看,密封类与枚举非常相似,而二者的主要差异在于,枚举类的每个枚举常量只有一个实例,而一个密封类可以有多个实例。因此,密封类的实例可以保存更丰富的、可变的状态,本文不再展开。