咱們常常須要在代碼中聲明一些有限集合,如: 網絡請求可能爲成功或失敗;用戶帳戶是高級用戶或普通用戶。html
咱們可使用枚舉來實現這類模型,但枚舉自身存在許多限制。枚舉類型的每一個值只容許有一個實例,同時枚舉也沒法爲每一個類型添加額外信息,例如,您沒法爲枚舉中的 "Error" 添加相關的 Exception 類型數據。網絡
固然也可使用一個抽象類而後讓一些類繼承它,這樣就能夠隨意擴展,但這會失去枚舉所帶來的有限集合的優點。而 sealed class (本文下稱 "密封類" ) 則同時包含了前面二者的優點 —— 抽象類表示的靈活性和枚舉裏集合的受限性。繼續閱讀接下來的內容能夠幫助你們更加深刻地瞭解密封類,您也能夠點擊觀看 視頻。app
和抽象類相似,密封類可用於表示層級關係。子類能夠是任意的類: 數據類、Kotlin 對象、普通的類,甚至也能夠是另外一個密封類。但不一樣於抽象類的是,您必須把層級聲明在同一文件中,或者嵌套在類的內部。ide
// 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>() }
嘗試在密封類所定義的文件外繼承類 (外部繼承),則會致使編譯錯誤:post
Cannot access ‘<init>’: it is private in Result
在 when 語句中,咱們經常須要處理全部可能的類型:this
when(result) { is Result.Success -> { } is Result.Error -> { } }
可是若是有人爲 Result 類添加了一個新的類型: InProgress:spa
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 語句在做爲表達式使用時,會經過編譯器報錯來強制要求必須覆蓋全部選項 (也就是說要窮舉):code
val action = when(result) { is Result.Success -> { } is Result.Error -> { } }
當表達式必須覆蓋全部選項時,添加 "is inProgress" 或者 "else" 分支。視頻
若是想要在使用 when 語句時得到相同的編譯器提示,能夠添加下面的擴展屬性:htm
val <T> T.exhaustive: T get() = this
這樣一來,只要給 when 語句添加 ".exhaustive",若是有分支未被覆蓋,編譯器就會給出以前同樣的錯誤。
when(result){ is Result.Success -> { } is Result.Error -> { } }.exhaustive
因爲一個密封類的全部子類型都是已知的,因此 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 是一個抽象類,而且包含兩個構造方法:
這意味着其餘的類沒法直接調用密封類的構造方法。若是咱們查看 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 幫忙避免類型錯誤吧。