密封类和密封接口为类层次结构提供了可控的继承机制。密封类的所有直接子类在编译时都是已知的。在定义密封类的模块和包之外,不会出现其他子类。相同的逻辑也适用于密封接口及其实现:一旦包含密封接口的模块编译完成,就无法再创建新的实现。
直接子类是指直接继承自其超类的类。间接子类是指从其超类经过不止一层继承而来的类。
当你将密封类和接口与 when
表达式结合使用时,你可以涵盖所有可能的子类的行为,并确保不会创建新的子类对代码产生负面影响。
Java 15
引入了类似的概念,密封类使用 sealed
关键字并搭配 permits
子句来定义受限的类层次结构。
要声明密封类或密封接口,请使用 sealed
修饰符:
// 创建一个密封接口。
sealed interface Error
// 创建一个实现密封接口 Error 的密封类。
sealed class IOError : Error
// 定义继承自密封类 IOError 的子类。
class FileReadError(val file: File) : IOError()
class DatabaseError(val source: DataSource) : IOError()
// 创建一个实现 Error 密封接口的单例对象。
object RuntimeError : Error
这个示例可以代表一个库的 API
,该 API
包含了一些错误类,以便库的使用者能够处理库可能抛出的错误。如果这类错误类的层次结构中包含在公共 API
里可见的接口或抽象类,那么其他开发者就可以在客户端代码中随意实现或扩展它们,这是无法阻止的。由于库并不知道在其外部声明的错误,因此无法像处理自身类那样一致地处理这些错误。
然而,使用密封的错误类层次结构,库的开发者可以确保自己知晓所有可能的错误类型,并且后续不会出现其他错误类型。
密封类本身始终是抽象类,因此不能直接实例化。不过,它可以包含或继承构造函数。这些构造函数并非用于创建密封类自身的实例,而是供其子类使用。以下是一个名为 Error
的密封类及其几个子类的示例,我们将对这些子类进行实例化操作:
sealed class Error(val message: String) {
class NetworkError : Error("Network failure")
class DatabaseError : Error("Database cannot be reached")
class UnknownError : Error("An unknown error has occurred")
}
fun main() {
val errors = listOf(
Error.NetworkError(),
Error.DatabaseError(),
Error.UnknownError()
)
// Network failure
// Database cannot be reached
// An unknown error has occurred
errors.forEach { println(it.message) }
}
可以在密封类中使用枚举类,利用枚举常量来表示状态并提供额外的详细信息。每个枚举常量仅作为单个实例存在,而密封类的子类可能有多个实例。在下面的示例中,密封类 Error
及其几个子类使用一个枚举来表示错误的严重程度。每个子类的构造函数都会初始化错误严重程度,并且可以改变其状态:
enum class ErrorSeverity { MINOR, MAJOR, CRITICAL }
sealed class Error(val severity: ErrorSeverity) {
class FileReadError(val file: File)
: Error(ErrorSeverity.MAJOR)
class DatabaseError(val source: DataSource)
: Error(ErrorSeverity.CRITICAL)
object RuntimeError
: Error(ErrorSeverity.CRITICAL)
// 这里可以添加更多的错误类型。
}
密封类的构造函数可以有两种可见性:protected
(默认)或 private
:
sealed class IOError {
// 密封类的构造函数默认具有 protected 可见性。
// 它在该类及其子类内部可见。
constructor() {}
// private 构造函数,仅在该类内部可见。
// 在密封类中使用 private 构造函数可以对实例化进行更严格的控制,
// 允许在类内部进行特定的初始化过程。
private constructor(description: String): this() {}
// 这将引发错误。
// 密封类中不允许使用 public 和 internal 构造函数。
// public constructor(code: Int): this() {}
}
密封类和密封接口的直接子类必须在同一个包中声明。它们可以是顶级类,也可以嵌套在任意数量的其他具名类、具名接口或具名对象内部。只要符合 Kotlin
中的常规继承规则,子类可以具有任何可见性。
密封类的子类必须具有正确的限定名称。它们不能是局部对象或匿名对象。
枚举类不能继承密封类,也不能继承其他任何类。不过,它们可以实现密封接口:
sealed interface Error
// 枚举类实现密封接口 Error。
enum class ErrorType : Error {
FILE_ERROR, DATABASE_ERROR
}
这些限制不适用于间接子类。如果密封类的直接子类没有被标记为密封类,那么它可以在其修饰符允许的范围内以任何方式被继承:
// 密封接口 Error 的实现只能位于同一包和同一模块中。
sealed interface Error
// 密封类 IOError 继承自 Error,并且只能在同一包内被继承。
sealed class IOError() : Error
// 开放类 CustomError 继承自 Error,并且在其可见的任何地方都可以被继承。
open class CustomError() : Error
when
表达式结合使用使用密封类的主要优势在与 when
表达式结合使用时得以体现。当 when
表达式与密封类一起使用时,编译器能够详尽地检查是否涵盖了所有可能的情况。在这种情况下,你无需添加 else
子句:
fun log(e: Error) = when(e) {
is Error.FileReadError -> println("${e.file}")
is Error.DatabaseError -> println("${e.source}")
Error.RuntimeError -> println("Runtime error")
// 由于涵盖了所有情况,因此不需要 else 子句。
}
在 when
表达式中使用密封类时,你还可以添加守卫条件,以便在单个分支中进行额外的检查。
使用密封类来表示应用程序中的不同 UI
状态。
sealed class UIState {
data object Loading : UIState()
data class Success(val data: String) : UIState()
data class Error(val exception: Exception) : UIState()
}
fun updateUI(state: UIState) {
when (state) {
is UIState.Loading -> showLoadingIndicator()
is UIState.Success -> showData(state.data)
is UIState.Error -> showError(state.exception)
}
}
高效处理各种支付方式。
sealed class Payment {
data class CreditCard(val number: String, val expiryDate: String) : Payment()
data class PayPal(val email: String) : Payment()
data object Cash : Payment()
}
fun processPayment(payment: Payment) {
when (payment) {
is Payment.CreditCard -> processCreditCardPayment(payment.number, payment.expiryDate)
is Payment.PayPal -> processPayPalPayment(payment.email)
is Payment.Cash -> processCashPayment()
}
}