Kotlin Academy · 课时

嵌套与组合密封层次结构

通过嵌套密封类型构建复杂的状态机。

第 4 / 4 课13 个步骤

嵌套与组合密封层次结构 是 CoddyKit 上的免费 Kotlin Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Kotlin Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Kotlin Academy 课程共包含 4 节课。

组合状态树

现实世界中的状态通常有多个维度。请嵌套密封类型,或组合不同的层次结构,以整洁地为复杂领域建模。

嵌套密封类

请在外层密封类型内部声明子密封类型。编译器仍会对整棵树执行完备性检查。

sealed class UiState {
    object Loading : UiState()
    sealed class Loaded : UiState() {
        data class Success(val items: List<String>) : Loaded()
        data class Empty(val message: String) : Loaded()
    }
    data class Error(val msg: String) : UiState()
}
fun render(s: UiState) = when (s) {
    UiState.Loading -> "loading"
    is UiState.Loaded.Success -> "${s.items.size} items"
    is UiState.Loaded.Empty -> "empty: ${s.message}"
    is UiState.Error -> "err: ${s.msg}"
}
fun main() {
    println(render(UiState.Loaded.Success(listOf("a", "b"))))
}

用于混入的密封接口

当某个状态具有多个相互正交的方面时,请使用密封接口——每个方面对应一个不同的接口。

sealed interface Authenticated
data class User(val id: Int) : Authenticated
object Guest
sealed interface Permission
object Read : Permission
object Write : Permission
fun describe(a: Authenticated, p: Permission): String = when {
    a is User && p is Write -> "user ${a.id} can write"
    a is User -> "user ${a.id} can read"
    else -> "?"
}
fun main() {
    println(describe(User(1), Write))
}

组合层次结构

传入两个密封值,并根据它们的组合进行切换——请使用带守卫条件的 when。

sealed class Theme { object Light : Theme(); object Dark : Theme() }
sealed class Lang { object En : Lang(); object Tr : Lang() }
fun greet(theme: Theme, lang: Lang) = when {
    theme is Theme.Dark && lang is Lang.Tr -> "Karanlik Merhaba"
    theme is Theme.Light && lang is Lang.Tr -> "Aydinlik Merhaba"
    theme is Theme.Dark -> "Dark Hello"
    theme is Theme.Light -> "Light Hello"
    else -> "?"
}
fun main() {
    println(greet(Theme.Dark, Lang.Tr))
}

带嵌套变体的状态机

将多步骤工作流建模为嵌套密封类型——每个步骤都有自己的变体。

sealed class Checkout {
    object Start : Checkout()
    sealed class Payment : Checkout() {
        object Pending : Payment()
        data class Failed(val reason: String) : Payment()
        object Captured : Payment()
    }
    object Shipped : Checkout()
    object Delivered : Checkout()
}
fun describe(c: Checkout) = when (c) {
    Checkout.Start -> "starting"
    Checkout.Payment.Pending -> "awaiting payment"
    is Checkout.Payment.Failed -> "payment failed: ${c.reason}"
    Checkout.Payment.Captured -> "paid"
    Checkout.Shipped -> "shipped"
    Checkout.Delivered -> "delivered"
}
fun main() {
    println(describe(Checkout.Payment.Failed("card declined")))
}

跨模块重用密封层次结构

最好在每个模块内定义密封类型。对于跨模块组合,请公开一个密封接口,让每个模块添加自己的实现。

sealed interface Notification
data class Email(val to: String) : Notification
data class Sms(val phone: String) : Notification
fun send(n: Notification) = when (n) {
    is Email -> "sending email to ${n.to}"
    is Sms -> "texting ${n.phone}"
}
fun main() {
    println(send(Email("a@example.com")))
}

公共基类属性

密封类可以声明公共属性——所有子类型都会继承这些属性。这样可以让状态和元数据保持 DRY。

sealed class HttpResponse(val code: Int) {
    class Ok(val body: String) : HttpResponse(200)
    class NotFound(val path: String) : HttpResponse(404)
    class ServerError(val cause: String) : HttpResponse(500)
}
fun describe(r: HttpResponse) = "[${r.code}] " + when (r) {
    is HttpResponse.Ok -> r.body
    is HttpResponse.NotFound -> "not found: ${r.path}"
    is HttpResponse.ServerError -> "fail: ${r.cause}"
}
fun main() {
    println(describe(HttpResponse.Ok("hello")))
}

为递归树建模

密封类型可以自然地描述表达式树等递归结构。

sealed class Expr {
    data class Num(val value: Int) : Expr()
    data class Add(val left: Expr, val right: Expr) : Expr()
    data class Mul(val left: Expr, val right: Expr) : Expr()
}
fun eval(e: Expr): Int = when (e) {
    is Expr.Num -> e.value
    is Expr.Add -> eval(e.left) + eval(e.right)
    is Expr.Mul -> eval(e.left) * eval(e.right)
}
fun main() {
    val expr = Expr.Add(Expr.Num(3), Expr.Mul(Expr.Num(4), Expr.Num(5)))
    println(eval(expr)) // 23
}

与泛型结合

密封类型可以是泛型,从而支持 Result<T> 这样的类型化结果封装。

sealed class ApiResult<out T> {
    data class Success<T>(val data: T) : ApiResult<T>()
    data class Failure(val message: String) : ApiResult<Nothing>()
}
fun handle(r: ApiResult<String>) = when (r) {
    is ApiResult.Success -> "got ${r.data}"
    is ApiResult.Failure -> "err: ${r.message}"
}
fun main() {
    println(handle(ApiResult.Success("hi")))
    println(handle(ApiResult.Failure("nope")))
}

反模式:嵌套过深

过深的嵌套会隐藏意图。如果密封树超过两层,请考虑将其扁平化,或将子树提取到各自的文件中。

sealed class Top {
    sealed class Mid : Top() {
        sealed class Deep : Mid() {
            object Leaf : Deep()
        }
    }
}
fun main() {
    val l = Top.Mid.Deep.Leaf
    println(l::class.simpleName)
}

通过组合进行组合

当两个状态维度相互独立时,请优先使用组合(封装两个密封值的数据类),而不是深度嵌套的层次结构。

sealed class Loading { object Active : Loading(); object Idle : Loading() }
sealed class Auth { data class Signed(val id: Int) : Auth(); object Anon : Auth() }
data class AppState(val loading: Loading, val auth: Auth)
fun main() {
    val s = AppState(Loading.Active, Auth.Signed(1))
    println(s)
}

快速检查

当两个状态维度相互独立时,建议采用什么方法?

回顾

嵌套密封类型以构建树形状态;通过组合(封装数据类)组合相互独立的密封层次结构。向密封基类添加公共属性,使用泛型表示类型化结果,并自然地为递归结构建模。请保持浅层嵌套。

免费开始

用 AI 导师学习 Kotlin — 免费

在浏览器中编写并运行真实代码,获得全天候 AI 导师的即时帮助,并在网页或应用中继续学习。

课程
51
课程
203

常见问题解答

「嵌套与组合密封层次结构」课时是免费的吗?

是的 — 「嵌套与组合密封层次结构」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Kotlin Academy 课程的其余内容,请升级到 CoddyKit PRO。 Kotlin Academy 课程共包含 4 节课。

「嵌套与组合密封层次结构」这节课中我会学到什么?

通过嵌套密封类型构建复杂的状态机。 你通过在浏览器中直接运行的动手代码来练习 Kotlin Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Kotlin Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Kotlin Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。

「嵌套与组合密封层次结构」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Kotlin Academy 课中编写并运行代码吗?

能。每节 Kotlin Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. sealed class 与 sealed interface:分别适用于何时
  2. 密封层次结构中的穷尽式 when
  3. 使用密封类建模界面状态
  4. 嵌套与组合密封层次结构
← 返回 Kotlin Academy