Kotlin 特色 sealed 关键字

sealed 意为密封的,可修饰类 class 和接口 interface,用来表示受限的继承结构。

Sealed Class

介绍

sealed class,密封类,密封类是一种特殊的抽象类,用于限制可以继承它的子类。

密封类具备最重要的一个特点:

  • 其子类可以出现在定义 sealed class 的不同文件中,但不允许出现在与之不同的 module 中,且需要保证 package 一致。

这样既可以避免 sealed class 文件过于庞大,又可以确保第三方库无法扩展你定义的 sealed class,达到限制类的扩展目的。事实上在早期版本中,只允许在 sealed class 内部或定义的同文件内扩展子类,这些限制在 Kotlin 1.5 中被逐步放开。

sealed class 还具有如下特点或限制:

  1. sealed class 是抽象类,可以拥有抽象方法,无法直接实例化。
  2. sealed class 的构造函数只能拥有两种可见性:默认情况下是 protected,还可以指定成 private,但不允许指定成 public。
  3. sealed class 子类可扩展局部以及匿名类以外的任意类型子类,包括普通 class、data classobject、sealed class 等,子类信息在编译期可知。
  4. sealed class 的实例,可配合 when 表达式进行判断,当所有类型覆盖后可以省略 else 分支。
  5. 当 sealed class 子类没有指定构造方法或定义任意属性的时候,建议定义成单例 object,因为即便实例化成多个实例,互相之间没有状态的区别。在 Kotlin 1.9 版本新增 data object 用于取代 object 的用法,编译器会提示"'sealed' sub-object can be converted to 'data object' ",toString() 不会打印 HashCode 等无用信息让输出更有意义。

使用

可以在 sealed class 内部定义子类:

复制代码
sealed class Language {
    //定义在内部
    data object English : Language()
    data class French(val str: String) : Language()
    class German(str: String) : Language()
}

还可以在外部定义子类:

复制代码
sealed class Language {
    //定义在内部
    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()

对于不同类型的扩展子类,when 表达式的判断亦不同:

  • 判断 sealed class 内部子类类型自然需要指定父类前缀

  • 判断 sealed class 外部子类类型自然无需指定前缀

  • object class 的话可以直接进行实例判断 ,也可以用 is 关键字判断类型匹配

  • 普通 class 类型的话则必须使用 is 关键字判断类型匹配

    复制代码
      fun demo(language: Language) {
          when (language) {
              Language.English -> {}
              is Language.French -> {}
              is Language.German -> {}
              Chinese -> {}
              is Japanese -> {}
              is Korean -> {}
          }
      }

我们知道Kotlin代码最终会编译成Java字节码的,让我们来看一下,上述的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,其内部子类按类型有所不同:

  • data object 类型在 class 内部集成了静态的 INSTANCE 实例,同时还有equalstoString 以及 hashCode 函数。
  • data class 类型在 class 内部集成了属性的 getequalscopytoString 以及 hashCode 函数。
  • class 类型仍是普通的 class。

而外部子类和同包名不同一文件下的子类则自然是定义在 Language 抽象类外部,内容也是跟上面一样的。

Sealed Interface

sealed interface,密封接口,和 sealed class 有几乎一样的特点。此外,还有额外的优势,可以帮助密封类、枚举类等类实现多继承和扩展性,比如搭配枚举,以处理更复杂的分类逻辑。

举例:Flappy Bird 游戏的过程中会产生很多 Action 来触发数据的计算以推动 UI 刷新以及游戏的进程,Action 可以用 enum class 来管理。

其中有些 Action 是关联的,有些则没有关联、不是同一层级。但是 enum class 默认扩展自 Enum 类,无法再嵌套 enum。

这将导致层级混乱、阅读性不佳,甚至有的时候功能相近的时候还得特意取个不同的名称。

复制代码
 enum class Action {
     Tick,
     // GameAction
     Start, Exit, Restart,
     // BirdAction
     Up, Down, HitGround, HitPipe, CrossedPipe,
     // PipeAction
     Move, Reset,
     // RoadAction
     // 防止和 Pipe 的 Action 重名导致编译出错,
     // 将功能差不多的 Road 移动和重置 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 进行层级拆分。更加清晰、亦不用担心重名。

复制代码
 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

使用的时候就可以对抽成的 Action 进行嵌套判断:

复制代码
 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 的区别

  • enum:enum 只是一个值(常量),每个 enum 常量只能以单例的形式存在。
  • sealed:sealed可以是一个值(定义成 data object 不携带数据),还可以是一个有状态的值(定义成 data class 携带数据)。sealed class 子类可以拥有多个实例,不受限制,每个均可以拥有自己的状态。
  • enum class 不能扩展自 sealed class 以及其他任何 Class,但可以实现 sealed interface,正如上面 Action 的举例。
相关推荐
索迪迈科技2 分钟前
java后端工程师进修ing(研一版 || day40)
java·开发语言·学习·算法
Zz_waiting.27 分钟前
Javaweb - 14.6 - Vue3 数据交互 Axios
开发语言·前端·javascript·vue·axios
索迪迈科技31 分钟前
影视APP源码 SK影视 安卓+苹果双端APP 反编译详细视频教程+源码
android·影视app源码·sk影视
孔丘闻言38 分钟前
python调用mysql
android·python·mysql
切糕师学AI39 分钟前
前后端分离架构中,Node.js的底层实现原理与线程池饥饿问题解析
前端·vue.js·node.js
武文斌771 小时前
arm启动代码总结
arm开发·嵌入式硬件·学习
妄小闲1 小时前
网页设计模板 HTML源码网站模板下载
前端·html
icebreaker1 小时前
tailwindcss 究竟比 unocss 快多少?
前端·css·github
卢叁1 小时前
Flutter之自定义TabIndicator
前端·flutter
我怕是好1 小时前
学习stm32 蓝牙
stm32·嵌入式硬件·学习