学习Kotlin中的密封类

704 阅读6分钟

Kotlin中的密封类

我们可以将密封类定义为枚举类和抽象类的独特混合体。我们的教程将从创建一个枚举类开始。当它变得越来越复杂时,我们将被迫切换到一个抽象类。

然后,密封类将最终介入,证明它更加强大和方便。

目标

本教程将展示一个密封类的最佳使用案例。

前提条件

  1. 对Kotlin编程语言有基本了解。
  2. 对面向对象编程概念的基本理解。
  3. 最好使用IntelliJ IDEA。如果无法使用,可以使用Kotlin Playground

内容列表

  • 什么是枚举类
    • 枚举类用例
    • 枚举类用例的总结
  • 什么是抽象类
    • 使用抽象类进行状态管理
    • 抽象类用例总结
  • 什么是密封类
    • 使用密封类进行状态管理
    • 充分发挥密封类的作用
  • 总结

什么是枚举类?

Enum "是 "枚举类型 "的简称。枚举类型 "来自于英文单词 "enumerate"。枚举的意思是将事物逐一列出。编程中的枚举类型是一个包含与枚举类型相同类型的元素列表的类型。

枚举类用例

打开main.kt

添加以下代码。

enum class LoadState{
    SUCCESS,
    LOADING,
    ERROR,
    IDLE
}

你已经创建了一个enum 类,它可以跟踪加载状态。

fun main() 旁边创建一个函数getStateOutput(loadState:LoadState) 。它将根据加载状态打印一个不同的字符串。

fun getStateOutput(loadState:LoadState){
    return when (loadState){

    }
}

使用项目快速修复来添加剩余的分支。

注意:项目快速修复只在IntelliJ IDEA中可用。要访问它,请点击红色下划线的when 关键字。在Windows中按Alt+Enter或在Mac中按Option+Enter。或者,你也可以点击弹出的lightbulb

选择add remaining branches

将以下字符串添加到分支中。

fun getStateOutput(loadState:LoadState){
    return when(loadState){
        LoadState.SUCCESS -> {
            println("Successfully loaded data")
        }
        LoadState.LOADING -> {
            println("Still loading...")
        }
        LoadState.ERROR -> {
            println("ERROR")
        }
        LoadState.IDLE -> {
            println("IDLE")
        }
    }
}

创建一个存储库的单子,它将模仿数据的获取。如图所示,使用object 关键字创建单子。

object Repository{
    private var loadState:LoadState = LoadState.IDLE
    //Data to be fetched when we startFetch()
    private var dataFetched:String? = null
    fun startFetch(){
        loadState = LoadState.LOADING
        dataFetched = "data"
    }
    fun finishFetch(){
        loadState = LoadState.SUCCESS
        //Return data fetched to its original state
        dataFetched = null
    }
    fun errorFetch(){
        loadState = LoadState.ERROR
    }
    fun getLoadState(): LoadState {
        return loadState
    }
}

现在玩一玩fun main() 中的这些方法。

fun main(){
    Repository.startFetch()
    getStateOutput(Repository.getLoadState())
    Repository.finishFetch()
    getStateOutput(Repository.getLoadState())
    Repository.errorFetch()
    getStateOutput(Repository.getLoadState())
}

output:

    Still loading...

    Successfully loaded data

    ERROR

枚举类用例的总结

当涉及到处理状态时,枚举类显然是有用的。它们可以很容易地跟踪事物。

现在考虑下面的情况。

如果你想根据获取的数据打印一个独特的成功信息,会怎么样?你可能还想在错误中捕捉独特的异常。

为了实现上述功能。

  • SUCCESS 将不得不发出一个独特的字符串
  • ERROR 将不得不发出一个独特的异常
  • LOADING 并 ,保持通用。IDLE

使用枚举类,我们将不得不这样做。

enum class LoadState{
    SUCCESS(val data:String),
    LOADING,
    ERROR(val exception:Exception),
    NOTLOADING
}

上面的代码产生了一个错误。这是因为你不能在枚举类中以不同方式表示constants

为了解决这个问题,你可以继承自一个抽象类。这允许你以不同的方式表示状态。

什么是抽象类?

抽象类是一个功能尚未实现的类。它可以被用来创建符合其协议的特定对象。

使用抽象类进行状态管理

用下面的代码替换枚举类LoadState

abstract class LoadState

data class Success(val dataFetched:String?):LoadState()
data class Error(val exception: Exception):LoadState()
object NotLoading:LoadState()
object Loading:LoadState()

Success 现在可以发出一个唯一的字符串 。 现在可以发出一个唯一的异常 。所有的状态都符合LoadState这个类型。然而,它们彼此之间有很大的不同。dataFetched Error exception

用这个替换fun getStateOutput 中的代码。

fun getStateOutput(loadState:LoadState){
    return when(loadState){
        is Error-> {
            println(loadState.exception.toString())
        }
        is Success -> {
            //If the dataFetched is null, return a default string.
            println(loadState.dataFetched?:"Ensure you startFetch first")
        }
        is Loading-> {
            println("Still loading...")
        }
        is NotLoading -> {
            println("IDLE")
        }
        //you have to add an else branch because the compiler cannot know whether the abstract class is exhausted.
        else-> println("invalid")
    }
}

用这个替换Repository 中的代码。

object Repository {
    private var loadState:LoadState = NotLoading
    private var dataFetched:String? = null

    fun startFetch(){
        loadState = Loading
        dataFetched = "Data"
    }
    fun finishFetch(){
        //passing the dataFetched to Success state.
        loadState = Success(dataFetched)
        dataFetched = null
    }
    fun errorFetch(){
        //passing a mock exception to the loadstate.
        loadState = Error(Exception("Exception"))
    }
    fun getLoadState(): LoadState {
        return loadState
    }
}

再次运行fun main()

output:

    Still loading...
    Data
    java.lang.Exception: Exception

抽象类用例的总结

通过扩展一个抽象类而不是使用枚举,你获得了以不同方式表示你的状态的自由。

遗憾的是,在这一过程中,你失去了一些基本的东西。枚举类型的限制集。现在,kotlin编译器陷入了困境。它无法判断when 语句的分支是否详尽。这就是为什么你不得不把else 作为一个分支加入。

这就是密封类出现的地方。它们提供了两个世界中最好的东西。你可以自由地以不同的方式表示你的状态,也可以得到枚举的典型限制。

什么是密封类

一个密封的类是一个抽象的类,它有一个受限制的类层次结构。继承自它的类必须与密封类在同一个文件中。

这提供了对继承的更多控制。它们受到限制,但也允许在状态表示方面的自由。

使用密封类进行状态管理

在你的代码中,将abstract class LoadState 中的abstract 关键字替换为sealed

之后,前往fun getStateOutput() 中的else 分支。

IntelliJ IDEA会出现以下错误。

    'when' is exhaustive so 'else' is redundant here

这是因为一个密封的类受到了限制。编译器可以判断出when 语句中的所有分支已经被列出。这意味着你可以安全地删除多余的else 分支。

充分发挥密封类的力量

密封类可以嵌套数据类、类、对象,也可以嵌套其他密封类。在处理其他密封类时,自动完成功能大放异彩。这是因为 IDE 可以检测这些类中的分支。

在你的密封类LoadState ,用这个密封类替换数据类Error

sealed class Error:LoadState(){
    data class CustomIOException(val ioException: IOException):Error()
    data class CustomNPEException(val npeException:NullPointerException):Error()
}

fun getStateOutput() 中删除is Error 分支,并允许 IDE 填充剩余的分支。

该函数的最终代码将看起来像这样。

fun getStateOutput(loadState:LoadState){
    return when(loadState){
        is Success -> {
            //If there dataFetched is null, return this default string.
            println(loadState.dataFetched?:"Ensure you startFetch first")
        }
        is Loading-> {
            println("Still loading...")
        }
        is NotLoading -> {
            println("IDLE")
        }
        is Error.CustomIOException -> {
            println(loadState.ioException.toString())
        }
        is Error.CustomNPEException -> {
            println(loadState.npeException.toString())
        }
    }
}

Repository 中,删除函数fun errorFetch()

添加以下属性。

    private val npeException = NullPointerException("There was a null pointer exception")
    private val ioException = IOException("There was an IO exception")

这些属性是我们将假装捕捉的异常。

将这些函数添加到Repository

    fun ioErrorFetchingData(){
        loadState = Error.CustomIOException(ioException)
    }
    fun npeErrorFetchingData(){
        loadState = Error.CustomNPEException(npeException)
    }

fun main() ,用下面的代码替换Repository.errorFetch

    Repository.ioErrorFetchingData()
    getStateOutput(Repository.getLoadState())
    Repository.npeErrorFetchingData()
    getStateOutput(Repository.getLoadState())

运行后,输出结果将看起来像。

    Still loading...
    Data
    java.io.IOException: There was an IO exception
    java.lang.NullPointerException: There was a null pointer exception

总结

你从枚举类开始。你看到了在处理状态时如何利用它们的限制。

接下来,你遇到了枚举类的一个限制。它们缺乏以不同方式表示状态的自由。你通过引入抽象类解决了这个问题。

最后,你意识到你失去了一些重要的东西。枚举类的限制。你通过用一个密封的类来取代抽象类,把它找回来了。