Kotlin Vocabulary | 密封类 sealed class

Kotlin Vocabulary | 密封类 sealed class

我们经常需要在代码中声明一些有限集合,如: 网络请求可能为成功或失败;用户账户是高级用户或普通用户。

我们可以使用枚举来实现这类模型,但枚举自身存在许多限制。枚举类型的每个值只允许有一个实例,同时枚举也无法为每个类型添加额外信息,例如,您无法为枚举中的 "Error" 添加相关的 Exception 类型数据。

当然也可以使用一个抽象类然后让一些类继承它,这样就可以随意扩展,但这会失去枚举所带来的有限集合的优势。而 sealed class (本文下称 "密封类" ) 则同时包含了前面两者的优势 —— 抽象类表示的灵活性和枚举里集合的受限性。继续阅读接下来的内容可以帮助大家更加深入地了解密封类,您也可以点击观看 视频

密封类的基本使用

和抽象类类似,密封类可用于表示层级关系。子类可以是任意的类: 数据类、Kotlin 对象、普通的类,甚至也可以是另一个密封类。但不同于抽象类的是,您必须把层级声明在同一文件中,或者嵌套在类的内部。

// Result.kt
sealed class Result<out T : Any> {
   data class Success<out T : Any>(val data: T) : Result<T>()
   data class Error(val exception: Exception) : Result<Nothing>()
}

尝试在密封类所定义的文件外继承类 (外部继承),则会导致编译错误:

Cannot access ‘<init>’: it is private in Result

忘记了一个分支?

在 when 语句中,我们常常需要处理所有可能的类型:

when(result) {
   is Result.Success -> { }
   is Result.Error -> { }
}

但是如果有人为 Result 类添加了一个新的类型: InProgress:

sealed class Result<out T : Any> { 
 
   data class Success<out T : Any>(val data: T) : Result<T>()
   data class Error(val exception: Exception) : Result<Nothing>()
   object InProgress : Result<Nothing>()
}

如果想要防止遗漏对新类型的处理,并不一定需要依赖我们自己去记忆或者使用 IDE 的搜索功能确认新添加的类型。使用 when 语句处理密封类时,如果没有覆盖所有情况,可以让编译器给我们一个错误提示。和 if 语句一样,when 语句在作为表达式使用时,会通过编译器报错来强制要求必须覆盖所有选项 (也就是说要穷举):

val action = when(result) {
  is Result.Success -> { }
  is Result.Error -> { }
}

当表达式必须覆盖所有选项时,添加 "is inProgress" 或者 "else" 分支。

如果想要在使用 when 语句时获得相同的编译器提示,可以添加下面的扩展属性:

val <T> T.exhaustive: T
    get() = this

这样一来,只要给 when 语句添加 ".exhaustive",如果有分支未被覆盖,编译器就会给出之前一样的错误。

when(result){
    is Result.Success -> { }
    is Result.Error -> { }
}.exhaustive

IDE 自动补全

由于一个密封类的所有子类型都是已知的,所以 IDE 可以帮我们补全 when 语句下的所有分支:

当涉及到一个层级复杂的密封类时,这个功能会显得更加好用,因为 IDE 依然可以识别所有的分支:

sealed class Result<out T : Any> {
  data class Success<out T : Any>(val data: T) : Result<T>()
  sealed class Error(val exception: Exception) : Result<Nothing>() {
     class RecoverableError(exception: Exception) : Error(exception)
     class NonRecoverableError(exception: Exception) : Error(exception)
  }
    object InProgress : Result<Nothing>()
}

不过这个功能无法用于抽象类,因为编译器并不知道继承的层级关系,所以 IDE 也就没办法自动生成分支。

工作原理

为何密封类会拥有这些特性?下面我们来看看反编译的 Java 代码都做了什么:

sealed class Result
data class Success(val data: Any) : Result()
data class Error(val exception: Exception) : Result()
 
@Metadata(
   ...
   d2 = {"Lio/testapp/Result;", "T", "", "()V", "Error", "Success", "Lio/testapp/Result$Success;", "Lio/testapp/Result$Error;" ...}
)
 
public abstract class Result {
   private Result() {
   }
 
   // $FF: synthetic method
   public Result(DefaultConstructorMarker $constructor_marker) {
      this();
   }
}

密封类的元数据中保存了一个子类的列表,编译器可以在需要的地方使用这些信息。

Result 是一个抽象类,并且包含两个构造方法:

  • 一个私有的默认构造方法
  • 一个合成构造方法,只有 Kotlin 编译器可以使用

这意味着其他的类无法直接调用密封类的构造方法。如果我们查看 Success 类反编译后的代码,可以看到它调用了 Result 的合成构造方法:

public final class Success extends Result {
   @NotNull
   private final Object data
 
   public Success(@NotNull Object data) {
      Intrinsics.checkParameterIsNotNull(data, "data");
      super((DefaultConstructorMarker)null);
      this.data = data;
   }
}

开始使用密封类来限制类的层级关系,让编译器和 IDE 帮忙避免类型错误吧。

版权声明

禁止一切形式的转载-禁止商用-禁止衍生 申请授权

脉脉不得语
脉脉不得语
Zhengzhou Website
Android Developer | https://androiddevtools.cn and https://androidweekly.io Funder | GDG Zhengzhou Funder & Ex Organizer | http://Toast.show(∞) Podcast Host

你已经成功订阅到 Android 开发技术周报
太棒了!接下来,完成检验以获得全部访问权限 Android 开发技术周报
欢迎回来!你已经成功登录了。
Unable to sign you in. Please try again.
成功!您的帐户已完全激活,您现在可以访问所有内容。
Error! Stripe checkout failed.
Success! Your billing info is updated.
Error! Billing info update failed.
🍗