kotlin系列知识卡片 - 密封类

前言

随着kotlin在Android日常开发中越来越频繁地使用,笔者觉得有必要对kotlin的一些特性做一些原理的梳理,所谓知其然知其所以然,才能更好让kotlin服务于我们的业务开发,所以会做一个kotlin系列,每个知识点简短,像个小卡片一样,便于理解和快速回顾。所有的源码为:Kotlin 标准库版本为1.9.10。

密封类

密封类(sealed class)是一种特殊的类,它的继承有更多的限制,密封类的字类数量是有限的,在编译时其所有的子类都是已知的,定义密封类的模块和包之外不得出现其他子类,编译时可以检查所有可能的字类。对比于枚举类,密封类和枚举都是类型有限个,但密封类只是类型有限,其可有多个实例,而枚举类则是常量,仅作为单个实例存在。

在不同的kotlin版本对密封类的使用有一些限制:

  • Kotlin 1.0: 密封类的子类必须位于与密封类相同的文件中。不可在其他文件中被定义。
  • Kotlin 1.1及更高版本:允许子类位于其他文件中,提高了代码的重用性和拓展性;子类可为嵌套类、顶层类、内部类

使用

密封接口/类及其子类

  • 场景一:单例对象+自定义实例
kotlin 复制代码
sealed interface IDirection {
    data class Custom(val dir: Int): IDirection
    object Top: IDirection
    object Left: IDirection
    object Right: IDirection
    object Bottom: IDirection
}

sealed class Type(val v: Int) {
    data object Type1: Type(1)
    data object Type2: Type(2)
    data object Type3: Type(3)
    data object Type4: Type(4)
}

数据的解析处理

kotlin 复制代码
private fun test(dir: IDirection) {
    when (dir) {
        is IDirection.Top -> { ... }
        is IDirection.Left -> { ... }
        is IDirection.Right -> { ... }
        is IDirection.Bottom -> { ... }
        is IDirection.Custom -> { ... }
    }
}

多层嵌套密封类

密封类数据结构定义

kotlin 复制代码
sealed interface IMsg {
    sealed interface Page: IMsg {
        object Init: Page
        data class StateChange(val state: Int): Page
    }
    
    sealed interface User: IMsg  {
        object Login: User
        object Logout: User
    }
}

多层级数据kotlin的解析处理

kotlin 复制代码
private fun test(msg: IMsg) {
    when (msg) {
        is IMsg.Page -> { dispatchPageMsg(it) }
        is IMsg.User -> { dispatchUserMsg(it) }
    }
}

/**
 * 处理IMsg.Page类型数据
 */
private fun dispatchPageMsg(msg: IMsg.Page) {
    when (msg) {
        is IMsg.Page.Init -> { ... }
        is IMsg.Page.StateChange -> { ... }
    }
}

/**
 * 处理IMsg.User类型数据
 */
private fun dispatchUserMsg(msg: IMsg.User) {
    when (msg) {
        is IMsg.User.Login -> { ... }
        is IMsg.User.Logout -> { ... }
    }
}

原理

单例类型的密封类对象

我们以【使用】部分提到的IDirectionIDirection.Top密封单例对象为例,反编译得到代码如下:

  • 定义一个INSTANCE作为Top的实例引用;
  • 将构造函数设置为私有构造,防止外部创建实例
  • 静态代码块初始化Top对象实例,静态代码块在类加载时就自动执行,确保INSTANCE对象在类载入时就完成初始化,且仅被初始化一次
java 复制代码
@Metadata(...)
public static final class Top implements IDirection {
   @NotNull
   public static final Top INSTANCE;

   private Top() {
   }

   static {
      Top var0 = new Top();
      INSTANCE = var0;
   }
}

可实例化的密封类

我们以【使用】部分提到的IDirectionIDirection.Custom密封单例对象为例,反编译得到代码如下:

从反编译后的源码中,我们可以看到代码中除了Custom类本身的构造及参数外,还默认实现了copy、toString、hashCode、equals等方法以支持kotlin的数据类的功能,因为kotlin的数据类支持解构声明,Custom中有一个dir参数,所以编译器生成了一个component1的函数方法。

java 复制代码
public static final class Custom implements IDirection {
   private final int dir;

   public final int getDir() {
      return this.dir;
   }

   public Custom(int dir) {
      this.dir = dir;
   }

   public final int component1() {
      return this.dir;
   }

   @NotNull
   public final Custom copy(int dir) {
      return new Custom(dir);
   }

   // $FF: synthetic method
   public static Custom copy$default(Custom var0, int var1, int var2, Object var3) {
      if ((var2 & 1) != 0) {
         var1 = var0.dir;
      }

      return var0.copy(var1);
   }

   @NotNull
   public String toString() {
      return "Custom(dir=" + this.dir + ")";
   }

   public int hashCode() {
      return Integer.hashCode(this.dir);
   }

   public boolean equals(@Nullable Object var1) {
      if (this != var1) {
         if (var1 instanceof Custom) {
            Custom var2 = (Custom)var1;
            if (this.dir == var2.dir) {
               return true;
            }
         }

         return false;
      } else {
         return true;
      }
   }
}

密封类的拓展受限规则如何实现?

密封接口的子类必须在与密封接口相同的模块和包中定义,所以在其他模块和包中是无法扩展密封接口的。但从反编译的代码中可以看到,密封接口编译器会将其编译为一个公共(public)的接口,从理论上是可以被外部访问和拓展的,但 Kotlin 编译器在编译密封接口时,会为其生成一个特殊的元数据(metadata),用于记录密封接口的限制信息。这个元数据会在编译时被 Kotlin 编译器检查,以确保密封接口的子类只能在与密封接口相同的模块和包中定义。

d1 字段中,\u0086\b 表示这是一个密封接口

java 复制代码
@Metadata(
   mv = {1, 9, 0},
   k = 1,
   d1 = {"\u0000&\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\b\n\u0002\b\u0006\n\u0002\u0010\u000b\n\u0000\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u000e\n\u0000\b\u0086\b\u0018\u00002\u00020\u0001B\r\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0002\u0010\u0004J\t\u0010\u0007\u001a\u00020\u0003HÆ\u0003J\u0013\u0010\b\u001a\u00020\u00002\b\b\u0002\u0010\u0002\u001a\u00020\u0003HÆ\u0001J\u0013\u0010\t\u001a\u00020\n2\b\u0010\u000b\u001a\u0004\u0018\u00010\fHÖ\u0003J\t\u0010\r\u001a\u00020\u0003HÖ\u0001J\t\u0010\u000e\u001a\u00020\u000fHÖ\u0001R\u0011\u0010\u0002\u001a\u00020\u0003¢\u0006\b\n\u0000\u001a\u0004\b\u0005\u0010\u0006¨\u0006\u0010"},
   d2 = {"LIDirection$Custom;", "LIDirection;", "dir", "", "(I)V", "getDir", "()I", "component1", "copy", "equals", "", "other", "", "hashCode", "toString", "", "calendarview_debug"}
)
public static final class Custom implements IDirection {
相关推荐
y = xⁿ1 小时前
MySQL:count(1)与count(*)有什么区别,深分页问题
android·数据库·mysql
程序员陆业聪3 小时前
Android启动全景图:一次冷启动背后到底发生了什么
android
安卓程序员_谢伟光5 小时前
m3颜色定义
android·compose
麻辣璐璐6 小时前
EditText属性运用之适配RTL语言和LTR语言的输入习惯
android·xml·java·开发语言·安卓
北京自在科技6 小时前
谷歌 Find Hub 网页端全面升级:电脑可直接管理追踪器与耳机
android·ios·安卓·findmy
Rush-Rabbit6 小时前
魅族21Pro刷ColorOS16.0操作步骤
android
爪洼传承人6 小时前
AI工具MCP的配置,慢sql优化
android·数据库·sql
学习使我健康6 小时前
MVP模式
android·github·软件工程
xiangxiongfly9157 小时前
Android MMKV
android·mmkv
北漂Zachary8 小时前
PHP3.0:改变Web开发的里程碑
android·php·laravel