kotlin系列知识卡片 - 密封类

2024-03-03  本文已影响0人  头秃到底

前言

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

密封类

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

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

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

使用

密封接口/类及其子类

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)
}

数据的解析处理

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

多层嵌套密封类

密封类数据结构定义

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的解析处理

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对象在类载入时就完成初始化,且仅被初始化一次
@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的函数方法。

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 表示这是一个密封接口

@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 {

上一篇下一篇

猜你喜欢

热点阅读