Kotlin Vocabulary | 密封類 sealed class

Android_開發者發表於2020-04-02

Kotlin Vocabulary | 密封類 sealed class
我們經常需要在程式碼中宣告一些有限集合,如: 網路請求可能為成功或失敗;使用者賬戶是高階使用者或普通使用者。

我們可以使用列舉來實現這類模型,但列舉自身存在許多限制。列舉型別的每個值只允許有一個例項,同時列舉也無法為每個型別新增額外資訊,例如,您無法為列舉中的 "Error" 新增相關的 Exception 型別資料。

當然也可以使用一個抽象類然後讓一些類繼承它,這樣就可以隨意擴充套件,但這會失去列舉所帶來的有限集合的優勢。而 sealed class (本文下稱 "密封類" ) 則同時包含了前面兩者的優勢 —— 抽象類表示的靈活性和列舉裡集合的受限性。繼續閱讀接下來的內容可以幫助大家更加深入地瞭解密封類,您也可以點選觀看下方視訊:

  • 騰訊視訊連結:

v.qq.com/x/page/b094…

  • Bilibili 視訊連結:

www.bilibili.com/video/BV1Nk…

密封類的基本使用

和抽象類類似,密封類可用於表示層級關係。子類可以是任意的類: 資料類、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 語句下的所有分支:

Kotlin Vocabulary | 密封類 sealed class
當涉及到一個層級複雜的密封類時,這個功能會顯得更加好用,因為 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>()
}
複製程式碼

Kotlin Vocabulary | 密封類 sealed class
不過這個功能無法用於抽象類,因為編譯器並不知道繼承的層級關係,所以 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 幫忙避免型別錯誤吧。

點選這裡瞭解更多關於用 Kotlin 進行 Android 開發的相關資料

Kotlin Vocabulary | 密封類 sealed class

相關文章