“密封类和接口表示受限制的类层次结构,提供对继承的更多控制。密封类的所有直接子类在编译时都是已知的。在定义密封类的模块和包之外不能出现其他子类。例如,第三方客户端不能在其代码中扩展您的密封类。因此,密封类的每个实例都有一个在编译该类时已知的有限集合中的类型。
参考官网:https://kotlinlang.org/docs/sealed-classes.html
如果您像我一样,第一次阅读时可能无法理解这一点,特别是在没有实际代码示例的情况下。
1、密封类的特性
密封类是抽象的,可以有抽象成员。
密封类不能直接实例化。
密封类不能有公共构造函数(默认情况下构造函数是私有的)。
密封类可以有子类,但它们必须在同一个文件中,或者嵌套在密封类声明中。
密封类的子类可以在密封类文件之外拥有子类。
2、Kotlin中的密封类:基本示例
下面是一个的密封类的基本示例:
sealed class Animal
上面代码的重要部分是“密封”修饰符。正如在密封类规则中提到的,密封类可以有子类,但它们必须都在同一个文件中,或者嵌套在密封类声明中。但是,密封类的子类不必在同一个文件中。
// 子类嵌套在密封类
sealed class Animal {
class Dog() : Animal()
class Cat() : Animal()
}
sealed class Animal
//子类不嵌套在密封类中,但必须在同一个文件类中
class Dog() : Animal()
class Cat() : Animal()
// Animal.kt
sealed class Animal() {
class Dog() : Animal()
class Cat() : Animal()
open class UnKnownAnimal() : Animal()
}
// OtherAnimal.kt
class Duck : Animal()//编译失败
class cow : Animal.UnKnownAnimal()//编译成功
使用密封类的主要好处是在when表达式中使用它们时发挥作用。如果可以验证语句是否涵盖所有情况,则不需要向语句添加else子句。但是,只有当您使用when作为表达式(使用结果)而不是作为语句时,这才有效。
//Animal.kt
sealed class Animal() {
class Dog() : Animal()
class Cat() : Animal()
}
//MainAnimal.kt
//编译失败,由于没有复关全部子类结果
fun getAnimalName(type:Animal):String{
return when (type){
is Animal.Cat -> "Cat"
}
}
上面这段代码失败了,因为我们必须实现所有类型。抛出的错误是:
'when' expression must be exhaustive, add necessary 'is Dog' branch or 'else' branch instead
这意味着我们必须在when表达式中包含所有类型的Animal。
fun getAnimalName(type:Animal):String{
return when (type){
is Animal.Cat -> "Cat"
is Animal.Dog -> "Dog"
}
}
3、Kotlin中的密封类:应用形式
a、带继承的密封类
密封类可以有子类,这允许您创建更复杂的类层次结构。下面是一个密封类层次结构的例子,它表示一个成功的结果和各种类型的错误:
sealed class Result<out T> {
data class Success<out T>(val data: T) : Result<T>()
sealed class Error : Result<Nothing>() {
data class NetworkError(val code: Int) : Error()
object Timeout : Error()
object Unknown : Error()
}
}
b、具有泛型类型的密封类
还可以将泛型类型与密封类一起使用,以创建更灵活和可重用的代码。下面是一个用泛型表示UI状态的密封类层次结构的例子:
sealed class UIState<out T> {
data class Success<out T>(val data: T) : UIState<T>()
data class Error(val message: String) : UIState<Nothing>()
object Loading : UIState<Nothing>()
}
c、带扩展函数的密封类
与枚举类似,也可以使用扩展函数向密封类添加功能。下面是一个扩展函数的例子,它将一个Result
密封的类实例映射到UIState
:
fun <T> Result<T>.toUIState(): UIState<T> {
return when (this) {
is Result.Success -> UIState.Success(data)
is Result.Error.NetworkError -> UIState.Error("Network error: $code")
is Result.Error.Timeout -> UIState.Error("Request timed out")
is Result.Error.Unknown -> UIState.Error("Unknown error")
}
}
4、思考
在面向对象编程中,有时候我们想要隐藏一些类的实现细节,只暴露其子类。
通常情况可以将基础类的构造函数声明为 protected 或 private 来实现。这样,只有子类才能调用基础类的构造函数来创建对象,外部类无法创建基础类的对象,从而隐藏了基础类的实现细节。
以下是一个示例,演示如何隐藏基础类只暴露其子类:
open class Animal protected constructor(val name: String) {
// 隐藏构造函数,只能被子类调用
open fun makeSound() {
println("This animal makes a sound.")
}
}
class Dog(name: String) : Animal(name) {
override fun makeSound() {
println("Woof!")
}
}
class Cat(name: String) : Animal(name) {
override fun makeSound() {
println("Meow!")
}
}
在上面的代码中,我们定义了一个名为 Animal
的基础类,并将其构造函数声明为 protected
,这意味着只有 Animal
的子类才能访问该构造函数。我们还定义了两个子类 Dog
和 Cat
,它们继承自 Animal
类。注意到,它们都没有定义自己的构造函数,因此它们将使用从 Animal 类继承的构造函数来创建对象。
通过这种方式,我们可以在不暴露 Animal
类的实现细节的情况下,使用 Dog
和 Cat
类来创建动物对象。例如:
val dog = Dog("Fido")
val cat = Cat("Whiskers")
在上面的代码中,我们使用 Dog
和 Cat
类来创建 dog
和 cat
对象,它们都是 Animal
类的子类,但是我们无法直接创建 Animal 对象。
需要注意的是,使用 protected
修饰构造函数的类可以被继承,这意味着在同一包内部可以创建子类;如果有这样的需求,不想要再让外部自定义子类只使用现有定义的子类,这种情况就无法满足了,这里我们就想到了密封类sealed
我们已经知道,在 Kotlin 中,可以使用 sealed 关键字定义一个密封类(sealed class),从而避免外部直接实例化该类。
密封类是一个抽象类,用于限制类的继承层级,只能在其内部定义子类。外部无法继承密封类,也无法直接实例化密封类,只能通过其子类来实例化。
以下是一个使用 sealed 关键字定义密封类的例子:
sealed class Animal {
abstract fun makeSound()
}
class Dog : Animal() {
override fun makeSound() {
println("汪汪汪")
}
}
class Cat : Animal() {
override fun makeSound() {
println("喵喵喵")
}
}
在上面的例子中,Animal
是一个密封类,它的子类只能在 Animal
内部定义,而外部无法继承或直接实例化 Animal
类。我们定义了 Dog
和 Cat
两个子类,用于表示狗和猫。
通过使用 sealed
关键字,我们可以限制类的继承层级,确保所有的子类都在密封类内部定义,从而避免了外部直接实例化该类的情况,也避免了外部去自定义扩展子类。