密封类(Sealed Classes)
为了限制一个类层次结构,可以将超类声明为
sealed
。
考虑使用不同交通工具的旅行者所进行的旅行:
// SealedClasses/UnSealed.kt
package withoutsealedclasses
import atomictest.eq
open class Transport
data class Train(
val line: String
): Transport()
data class Bus(
val number: String,
val capacity: Int
): Transport()
fun travel(transport: Transport) =
when (transport) {
is Train ->
"Train ${transport.line}"
is Bus ->
"Bus ${transport.number}: " +
"size ${transport.capacity}"
else -> "$transport is in limbo!"
}
fun main() {
listOf(Train("S1"), Bus("11", 90))
.map(::travel) eq
"[Train S1, Bus 11: size 90]"
}
Train
和 Bus
分别包含有关它们的 Transport
模式的不同细节。
travel()
函数包含一个 when
表达式,该表达式会发现 transport
参数的确切类型。Kotlin 需要默认的 else
分支,因为可能会存在 Transport
的其他子类。
travel()
显示了向下转型的固有问题所在。假设您继承了 Tram
作为 Transport
的新类型。如果这样做,travel()
仍然会编译和运行,没有任何提示您应该修改它来检测 Tram
。如果您的代码中分散了许多向下转型的实例,那么这将成为维护的挑战。
我们可以通过使用 sealed
关键字来改善这种情况。在定义 Transport
时,将 open class
替换为 sealed class
:
// SealedClasses/SealedClasses.kt
package sealedclasses
import atomictest.eq
sealed class Transport
data class Train(
val line: String
) : Transport()
data class Bus(
val number: String,
val capacity: Int
) : Transport()
fun travel(transport: Transport) =
when (transport) {
is Train ->
"Train ${transport.line}"
is Bus ->
"Bus ${transport.number}: " +
"size ${transport.capacity}"
}
fun main() {
listOf(Train("S1"), Bus("11", 90))
.map(::travel) eq
"[Train S1, Bus 11: size 90]"
}
sealed
类的所有直接子类必须位于与基类相同的文件中。
尽管 Kotlin 强制您在 when
表达式中详尽检查所有可能的类型,但 travel()
中的 when
现在不再需要 else
分支。由于 Transport
是 sealed
,Kotlin 知道除了此文件中存在的那些类之外,没有其他 Transport
的子类。when
表达式现在是详尽无遗的,不需要 else
分支。
sealed
类层次结构可以发现在添加新的子类时的错误。当引入新的子类时,您必须更新使用现有层次结构的所有代码。在 UnSealed.kt
中的 travel()
函数将继续工作,因为 else
分支在未知类型的交通工具上产生 "$transport is in limbo!"
。然而,这可能不是您想要的行为。
sealed
类揭示了在添加新的子类(如 Tram
)时要修改的所有位置。在没有进行额外更改的情况下,SealedClasses.kt
中的 travel()
函数将无法编译,如果我们引入 Tram
类的话。sealed
关键字使忽视问题变得不可能,因为会得到编译错误。
sealed
关键字使向下转型变得更加容易,但您仍然应该对过多使用向下转型的设计保持怀疑。通常有一种更好和更清晰的方式使用多态来编写代码。
sealed
vs. abstract
在这里,我们展示了 abstract
和 sealed
类允许相同类型的函数、属性和构造函数:
// SealedClasses/SealedVsAbstract.kt
package sealedclasses
abstract class Abstract(val av: String) {
open fun concreteFunction() {}
open val concreteProperty = ""
abstract fun abstractFunction(): String
abstract val abstractProperty: String
init {}
constructor(c: Char) : this(c.toString())
}
open class Concrete() : Abstract("") {
override fun concreteFunction() {}
override val concreteProperty = ""
override fun abstractFunction() = ""
override val abstractProperty = ""
}
sealed class Sealed(val av: String) {
open fun concreteFunction() {}
open val concreteProperty = ""
abstract fun abstractFunction(): String
abstract val abstractProperty: String
init {}
constructor(c: Char) : this(c.toString())
}
open class SealedSubclass() : Sealed("") {
override fun concreteFunction() {}
override val concreteProperty = ""
override fun abstractFunction() = ""
override val abstractProperty = ""
}
fun main() {
Concrete()
SealedSubclass()
}
sealed
类基本上是一个带有额外约束的 abstract
类,所有直接子类必须定义在相同的文件中。
sealed
类的间接子类可以在单独的文件中定义:
// SealedClasses/ThirdLevelSealed.kt
package sealedclasses
class ThirdLevel : SealedSubclass()
ThirdLevel
并未直接继承自 Sealed
,因此无需将其放置在 SealedVsAbstract.kt
中。
尽管 sealed
接口似乎是一个有用的构造,但 Kotlin 并未提供它,因为无法阻止 Java 类实现相同的接口。
枚举子类
当一个类是 sealed
时,您可以轻松地迭代其子类:
// SealedClasses/SealedSubclasses.kt
package sealedclasses
import atom
ictest.eq
sealed class Top
class Middle1 : Top()
class Middle2 : Top()
open class Middle3 : Top()
class Bottom3 : Middle3()
fun main() {
Top::class.sealedSubclasses
.map { it.simpleName } eq
"[Middle1, Middle2, Middle3]"
}
创建一个类会生成一个 类对象。您可以访问该类对象的属性和成员函数来发现信息,并创建和操作该类的对象。::class
会产生一个类对象,因此 Top::class
会生成 Top
的类对象。
类对象的属性之一是 sealedSubclasses
,它期望 Top
是一个 sealed
类(否则会产生一个空列表)。sealedSubclasses
会产生所有这些子类的类对象。请注意,结果中仅显示 Top
的直接子类。
类对象的 toString()
稍微冗长。通过使用 simpleName
属性,我们只生成类名本身。
sealedSubclasses
使用 反射,需要在类路径中添加依赖项 kotlin-reflection.jar
。反射是一种动态发现和使用类的特征的方法。
当构建多态系统时,sealedSubclasses
可能是一个重要的工具。它可以确保新的类将自动包含在所有适当的操作中。然而,由于它在运行时发现子类,因此可能会对您的系统产生性能影响。如果您遇到速度问题,请确保使用分析器来发现 sealedSubclasses
是否可能是问题的原因(随着您学习使用分析器,您会发现性能问题通常并不在您猜测的地方)。
练习和解答可以在 www.AtomicKotlin.com 上找到。