Kotlin 特色 sealed 關(guān)鍵字

sealed 意為密封的,可修飾類 class 和接口 interface,用來(lái)表示受限的繼承結(jié)構(gòu)。

Sealed Class

介紹

sealed class,密封類,密封類是一種特殊的抽象類,用于限制可以繼承它的子類。

密封類具備最重要的一個(gè)特點(diǎn):

  • 其子類可以出現(xiàn)在定義 sealed class 的不同文件中,但不允許出現(xiàn)在與之不同的 module 中,且需要保證 package 一致。

這樣既可以避免 sealed class 文件過(guò)于龐大,又可以確保第三方庫(kù)無(wú)法擴(kuò)展你定義的 sealed class,達(dá)到限制類的擴(kuò)展目的。事實(shí)上在早期版本中,只允許在 sealed class 內(nèi)部或定義的同文件內(nèi)擴(kuò)展子類,這些限制在 Kotlin 1.5 中被逐步放開(kāi)。

sealed class 還具有如下特點(diǎn)或限制:

  1. sealed class 是抽象類,可以擁有抽象方法,無(wú)法直接實(shí)例化。
  2. sealed class 的構(gòu)造函數(shù)只能擁有兩種可見(jiàn)性:默認(rèn)情況下是 protected,還可以指定成 private,但不允許指定成 public。
  3. sealed class 子類可擴(kuò)展局部以及匿名類以外的任意類型子類,包括普通 class、data classobject、sealed class 等,子類信息在編譯期可知。
  4. sealed class 的實(shí)例,可配合 when 表達(dá)式進(jìn)行判斷,當(dāng)所有類型覆蓋后可以省略 else 分支。
  5. 當(dāng) sealed class 子類沒(méi)有指定構(gòu)造方法或定義任意屬性的時(shí)候,建議定義成單例 object,因?yàn)榧幢銓?shí)例化成多個(gè)實(shí)例,互相之間沒(méi)有狀態(tài)的區(qū)別。在 Kotlin 1.9 版本新增 data object 用于取代 object 的用法,編譯器會(huì)提示“'sealed' sub-object can be converted to 'data object' ”,toString() 不會(huì)打印 HashCode 等無(wú)用信息讓輸出更有意義。

使用

可以在 sealed class 內(nèi)部定義子類:

sealed class Language {
    //定義在內(nèi)部
    data object English : Language()
    data class French(val str: String) : Language()
    class German(str: String) : Language()
}

還可以在外部定義子類:

sealed class Language {
    //定義在內(nèi)部
    data object English : Language()
    data class French(val str: String) : Language()
    class German(str: String) : Language()
}

//定義在同一文件中
data object Chinese : Language()
data class Japanese(val str: String) : Language()

此外還可以定義在同包名不同一文件下

//定義在同包名不同一文件下
data class Korean(val str: String) : Language()

對(duì)于不同類型的擴(kuò)展子類,when 表達(dá)式的判斷亦不同:

  • 判斷 sealed class 內(nèi)部子類類型自然需要指定父類前綴
  • 判斷 sealed class 外部子類類型自然無(wú)需指定前綴
  • object class 的話可以直接進(jìn)行實(shí)例判斷,也可以用 is 關(guān)鍵字判斷類型匹配
  • 普通 class 類型的話則必須使用 is 關(guān)鍵字判斷類型匹配
    fun demo(language: Language) {
        when (language) {
            Language.English -> {}
            is Language.French -> {}
            is Language.German -> {}
            Chinese -> {}
            is Japanese -> {}
            is Korean -> {}
        }
    }

我們知道Kotlin代碼最終會(huì)編譯成Java字節(jié)碼的,讓我們來(lái)看一下,上述的Kotlin代碼反編譯之后是怎么樣的:

public abstract class Language {
    public /* synthetic */ Language(DefaultConstructorMarker defaultConstructorMarker) {
        this();
    }

    private Language() {
    }

    // subclass:data object
    public static final class English extends Language {
        public static final English INSTANCE = new English();

        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (!(obj instanceof English)) {
                return false;
            }
            English english = (English) obj;
            return true;
        }

        public int hashCode() {
            return -765073291;
        }

        public String toString() {
            return "English";
        }

        private English() {
            super(null);
        }
    }

    // subclass:data class
    public static final class French extends Language {
        private final String str;

        public static /* synthetic */ French copy$default(French french, String str2, int i, Object obj) {
            if ((i & 1) != 0) {
                str2 = french.str;
            }
            return french.copy(str2);
        }

        public final String component1() {
            return this.str;
        }

        public final French copy(String str2) {
            Intrinsics.checkNotNullParameter(str2, "str");
            return new French(str2);
        }

        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            return (obj instanceof French) && Intrinsics.areEqual(this.str, ((French) obj).str);
        }

        public int hashCode() {
            return this.str.hashCode();
        }

        public String toString() {
            return "French(str=" + this.str + ')';
        }

        public French(String str2) {
            super(null);
            Intrinsics.checkNotNullParameter(str2, "str");
            this.str = str2;
        }

        public final String getStr() {
            return this.str;
        }
    }

    // subclass:class
    public static final class German extends Language {
        public German(String str) {
            super(null);
            Intrinsics.checkNotNullParameter(str, "str");
        }
    }
}

可以看到 sealed class 本身被編譯為 abstract class,其內(nèi)部子類按類型有所不同:

  • data object 類型在 class 內(nèi)部集成了靜態(tài)的 INSTANCE 實(shí)例,同時(shí)還有equalstoString 以及 hashCode 函數(shù)。
  • data class 類型在 class 內(nèi)部集成了屬性的 getequalscopytoString 以及 hashCode 函數(shù)。
  • class 類型仍是普通的 class。

而外部子類和同包名不同一文件下的子類則自然是定義在 Language 抽象類外部,內(nèi)容也是跟上面一樣的。

Sealed Interface

sealed interface,密封接口,和 sealed class 有幾乎一樣的特點(diǎn)。此外,還有額外的優(yōu)勢(shì),可以幫助密封類、枚舉類等類實(shí)現(xiàn)多繼承和擴(kuò)展性,比如搭配枚舉,以處理更復(fù)雜的分類邏輯。

舉例:Flappy Bird 游戲的過(guò)程中會(huì)產(chǎn)生很多 Action 來(lái)觸發(fā)數(shù)據(jù)的計(jì)算以推動(dòng) UI 刷新以及游戲的進(jìn)程,Action 可以用 enum class 來(lái)管理。

其中有些 Action 是關(guān)聯(lián)的,有些則沒(méi)有關(guān)聯(lián)、不是同一層級(jí)。但是 enum class 默認(rèn)擴(kuò)展自 Enum 類,無(wú)法再嵌套 enum。

這將導(dǎo)致層級(jí)混亂、閱讀性不佳,甚至有的時(shí)候功能相近的時(shí)候還得特意取個(gè)不同的名稱。

 enum class Action {
     Tick,
     // GameAction
     Start, Exit, Restart,
     // BirdAction
     Up, Down, HitGround, HitPipe, CrossedPipe,
     // PipeAction
     Move, Reset,
     // RoadAction
     // 防止和 Pipe 的 Action 重名導(dǎo)致編譯出錯(cuò),
     // 將功能差不多的 Road 移動(dòng)和重置 Action 定義加上了前綴
     RoadMove, RoadReset
 }
 
 fun dispatch(action: Action) {
     when (action) {
         Action.Tick -> {}
 
         Action.Start -> {}
         Action.Exit -> {}
         Action.Restart -> {}
 
         Action.Up -> {}
         Action.Down -> {}
         Action.HitGround -> {}
         Action.HitPipe -> {}
         Action.CrossedPipe -> {}
 
         Action.Move -> {}
         Action.Reset -> {}
 
         Action.RoadMove -> {}
         Action.RoadReset -> {}
     }
 }

借助 sealed interface 我們可以給抽出 interface,并將 enum 進(jìn)行層級(jí)拆分。更加清晰、亦不用擔(dān)心重名。

 sealed interface Action
 
 enum class GameAction : Action {
     Start, Exit, Restart
 }
 
 enum class BirdAction : Action {
     Up, Down, HitGround, HitPipe, CrossedPipe
 }
 
 enum class PipeAction : Action {
     Move, Reset
 }
 
 enum class RoadAction : Action {
     Move, Reset
 }
 
 object Tick: Action

使用的時(shí)候就可以對(duì)抽成的 Action 進(jìn)行嵌套判斷:

 fun dispatch(action: Action) {
     when (action) {
         Tick -> {}
         
         is GameAction -> {
             when (action) {
                 GameAction.Start -> {}
                 GameAction.Exit -> {}
                 GameAction.Restart -> {}
             }
         }
         is BirdAction -> {
             when (action) {
                 BirdAction.Up -> {}
                 BirdAction.Down -> {}
                 else -> {}
             }
         }
         is PipeAction -> {
             when (action) {
                 PipeAction.Move -> {}
                 PipeAction.Reset -> {}
             }
         }
         is RoadAction -> {
             when (action) {
                 RoadAction.Move -> {}
                 RoadAction.Reset -> {}
             }
         }
     }
 }

sealed 與 enum 的區(qū)別

  • enum:enum 只是一個(gè)值(常量),每個(gè) enum 常量只能以單例的形式存在。
  • sealed:sealed可以是一個(gè)值(定義成 data object 不攜帶數(shù)據(jù)),還可以是一個(gè)有狀態(tài)的值(定義成 data class 攜帶數(shù)據(jù))。sealed class 子類可以擁有多個(gè)實(shí)例,不受限制,每個(gè)均可以擁有自己的狀態(tài)。
  • enum class 不能擴(kuò)展自 sealed class 以及其他任何 Class,但可以實(shí)現(xiàn) sealed interface,正如上面 Action 的舉例。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容