几年前,我用Kotlin开发了一个基于CamundaBPMN的应用程序,帮助我管理我的会议提交工作流程。它在Trello中跟踪我的提交材料,并在Google Calendar和Google Sheet中同步它们。谷歌日历提供了一个REST API。作为REST API,它到处都是杂乱无章的String ,下面是代码的摘录:
fun execute(color: String, availability: String) {
findCalendarEntry(client, google, execution.conference)?.let {
it.colorId = color (1)
it.transparency = availability (2)
client.events()
.update(google.calendarId, it.id, it).execute()
}
}
| 1 | 设置事件的颜色。有效值是 "0"、"1"、...到 "11" |
| 2 | 设置事件的可用性。有效值是"transparent" 和"opaque" |
然而,我的经验告诉我,我倾向于使用强势打字。我还想避免打字错误。我想在这篇文章中列出一些使用String 的替代方法。
常量
书中最古老的技巧,在大多数语言中都可以使用,就是定义常量。在Java 5之前,开发人员经常使用这种方法,因为这是唯一可用的方法,它看起来像这样:
const val Default = "0"
const val Blue = "1"
const val Green = "2"
const val Free = "transparent"
const val Busy = "opaque"
我们现在可以相应地调用该函数:
execute(Blue, Busy)
常量有助于处理错别字。反过来说,它们不能强制执行强类型化:
execute(Blue, Red) (1)
execute(Free, Red) (2)
| 1 | 传递两种颜色,但编译器没有问题 |
| 2 | 颠倒参数;编译器仍然没有问题。 |
类型别名
类型别名背后的想法是将现有类型的名称别到更有意义的地方:
typealias Color = String
typealias Availability = String
有了这个,我们可以改变函数的签名:
fun execute(color: Color, availability: Availability) {
// ...
}
不幸的是,类型别名只是表面现象。不管是什么别名,String ,还是String 。我们仍然可以写出不正确的代码:
execute(Blue, Red) (1)
execute(Free, Red) (1)
| 1 | 没有任何改进 |
枚举
无论是在Java还是Kotlin中,枚举都是走向强类型的第一步。我相信大多数开发者都知道它们。让我们改变我们的代码以使用枚举:
enum class Color(val id: String) {
Default("0"),
Blue("1"),
Green("2"),
}
enum class Availability(val value: String) {
Free("transparent"),
Busy("opaque"),
}
我们需要对函数进行相应的修改,包括签名和实现:
fun execute(color: Color, availability: Availability) {
findCalendarEntry(client, google, execution.conference)?.let {
it.colorId = color.id (1)
it.transparency = availability.value (1)
client.events()
.update(google.calendarId, it.id, it).execute()
}
}
| 1 | 提取被枚举包裹的值。enum |
枚举的使用强制了强类型:
execute(Color.Blue, Availability.Busy) (1)
execute(Color.Blue, Color.Red) (2)
execute(Availability.Free, Color.Blue) (2)
| 1 | 编译 |
| 2 | 不能编译! |
内联类
最近Kotlin的一个特点是完全致力于强类型化:内联类。一个内联类包裹了一个单一的 "原始 "值,比如Int 或String 。请看下面这个类:
data class Person(givenName: String, familyName: String)
这个类的调用者将不得不记住第一个参数是给定的名字还是家族的名字。Kotlin已经通过允许命名参数来提供帮助:
val p = Person(givenName = "John", familyName = "Doe")
然而,我们可以通过将String 包装在两个不同的值类型中来改进上面的片段,每个角色一个:
@JvmInline value class GivenName(value: String)
@JvmInline value class FamilyName(value: String)
val p = Person(GivenName("John"), FamilyName("Doe"))
在这一点上,我们不能把名字换成姓氏,反之亦然。同样,我们可以在我们的例子中使用价值类,并在一个同伴对象中定义可能的值:
@JvmInline
value class Color(val id: String) {
companion object {
val Default = Color("0")
val Blue = Color("1")
val Green = Color("2")
}
}
@JvmInline
value class Availability(val value: String) {
companion object {
val Free = Availability("transparent")
val Busy = Availability("opaque")
}
}
execute(Color.Blue, Availability.Busy) (1)
execute(Color.Blue, Color.Red) (2)
execute(Availability.Free, Color.Blue) (2)
| 1 | 编译 |
| 2 | 不能编译! |
封闭类
封闭类是另一种执行强类型的可能方式。其限制是我们需要在同一个包中定义一个密封类的所有子类。不能有任何第三方的继承。实际上,这使得类对于你的代码来说是open ,对于客户代码来说是final
我们不是像价值类那样定义一个类型和它的几个实例,而是直接定义不同的类型:
sealed class Color(val id: String) {
object Default: Color("0")
object Blue: Color("1")
object Green: Color("2")
}
sealed class Availability(val value: String) {
object Free : Availability("transparent")
object Busy : Availability("opaque")
}
execute(Color.Blue, Availability.Busy) (1)
execute(Color.Blue, Color.Red) (2)
execute(Availability.Free, Color.Blue) (2)
| 1 | 编译 |
| 2 | 不能编译! |
请注意,我在它们各自的父类中定义了这些对象。根据你的情况,你可能想把它们变成顶级的:
sealed class Color(val id: String)
object Default: Color("0")
object Blue: Color("1")
object Green: Color("2")
sealed class Availability(val value: String)
object Free : Availability("transparent")
object Busy : Availability("opaque")
execute(Blue, Busy)
总结
Kotlin提供了几种在API上执行强类型的选择:枚举、值类和密封类。
虽然大多数开发者对枚举很熟悉,但我建议考虑值类和密封类,因为它们会带来额外的好处。