Kotlin 特色之 Sealed Class 和 Interface

6083c358fd01a3e51f7d7475d2c5ef7e.jpeg

/   今日科技快讯   /

近日,我国在太原卫星发射中心使用长征六号改运载火箭,成功将云海三号卫星发射升空,卫星顺利进入预定轨道,发射任务获得圆满成功。该卫星主要用于开展大气海洋环境要素探测、空间环境探测、防灾减灾和科学试验等。此次任务是长征系列运载火箭的第448次飞行。

/   作者简介   /

本篇文章转自TechMerger的博客,文章主要分享了 Kotlin 中 Sealed class 的相关内容,相信会对大家有所帮助!

原文地址:

https://juejin.cn/post/7160111185201725476

/   前言   /

sealed class 以及 1.5 里新增的 sealed interface 可谓是 Kotlin 语言的一大特色,其在类型判断、扩展和实现的限制场景里非常好用。

本文将从特点、场景和原理等角度综合分析 sealed 语法。

  • Sealed Class

  • Sealed Interface

  • Sealed Class & Interface VS Enum

  • Sealed Class VS Interface

/   Sealed class   /

sealed class,密封类。具备最重要的一个特点:

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

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

如果在不同 module 或 package 中扩展子类的话,IDE 会显示如下的提示和编译错误:

Inheritor of sealed class or interface declared in package xxx but it must be in package xxx where base class is declared

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

sealed class 是抽象类,可以拥有抽象方法,无法直接实例化。否则,编译器将提示如下:

Sealed types cannot be instantiated

sealed class 的构造函数只能拥有两种可见性:默认情况下是 protected,还可以指定成 private,public 是不被允许的。

Constructor must be private or protected in sealed class

sealed class 子类可扩展局部以及匿名类以外的任意类型子类,包括普通 class、data class、object、sealed class 等,子类信息在编译期可知。假使匿名类扩展自 sealed class 的话,会弹出错误提示:

This type is sealed, so it can be inherited by only its own nested classes or objects

sealed class 的实例,可配合 when 表达式进行判断,当所有类型覆盖后可以省略 else 分支。如果没有覆盖所有类型,也没有 else 统筹则会发生编译警告或错误

1.7 以前

Non-exhaustive 'when' statements on sealed class/interface will be prohibited in 1.7.

1.7 及以后

'when' expression must be exhaustive, add ...

当 sealed class 没有指定构造方法或定义任意属性的时候,建议子类定义成单例,因为即便实例化成多个实例,互相之间没有状态的区别:

'sealed' subclass has no state and no overridden 'equals()'

下面结合代码看下 sealed class 的使用和原理。

示例代码:

// TestSealed.kt
 sealed class GameAction(times: Int) {
     // Inner of Sealed Class
     object Start : GameAction(1)
     data class AutoTick(val time: Int) : GameAction(2)
     class Exit : GameAction(3)
 }

除了在 sealed class 内嵌套子类外,还可以在外部扩展子类:

// TestSealed.kt
 sealed class GameAction(times: Int) {
     ...
 }
 
 // Outer of Sealed Class
 object Restart : GameAction(4)

除了可以在同文件下 sealed class 外扩展子类外,还可以在同包名不同文件下扩展。

// TestExtendedSealedClass.kt
 // Outer of Sealed Class file
 class TestExtendedSealedClass: GameAction(5)

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

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

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

  • 普通 class 类型的话则必须加上 is 关键字

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

class TestSealed {
     fun test(gameAction: GameAction) {
         when (gameAction) {
             GameAction.Start -> {}
             // is GameAction.Start -> {}
             is GameAction.AutoTick -> {}
             is GameAction.Exit -> {}
 
             Restart -> {}
             is TestExtendedSealedClass -> {}
         }
     }
 }

如下反编译的 Kotlin 代码可以看到 sealed class 本身被编译为 abstract class。

扩展自其的内部子类按类型有所不同:

  • object class 在 class 内部集成了静态的 INSTANCE 实例

  • 普通 class 仍是普通 class

  • data Class 则是在 class 内部集成了属性的 get、toString 以及 hashCode 函数

public abstract class GameAction {
    private GameAction(int times) { }
 
    public GameAction(int times, DefaultConstructorMarker $constructor_marker) {
       this(times);
    }

    // subclass:object
    public static final class Start extends GameAction {
       @NotNull
       public static final GameAction.Start INSTANCE;
 
       private Start() {
          super(1, (DefaultConstructorMarker)null);
       }
 
       static {
          GameAction.Start var0 = new GameAction.Start();
          INSTANCE = var0;
       }
    }
 
    // subclass:class
    public static final class Exit extends GameAction {
       public Exit() {
          super(3, (DefaultConstructorMarker)null);
       }
    }
 
    // subclass:data class
    public static final class AutoTick extends GameAction {
       private final int time;
 
       public final int getTime() {
          return this.time;
       }
 
       public AutoTick(int time) {
          super(2, (DefaultConstructorMarker)null);
          this.time = time;
       }
       ...
       @NotNull
       public String toString() {
          return "AutoTick(time=" + this.time + ")";
       }
 
       public int hashCode() { ... }
 
       public boolean equals(@Nullable Object var1) { ... }
    }
 }
而外部子类则自然是定义在 GameAction 抽象类外部。
public abstract class GameAction {
    ...
 }
 
 public final class Restart extends GameAction {
    @NotNull
    public static final Restart INSTANCE;
 
    private Restart() {
       super(4, (DefaultConstructorMarker)null);
    }
 
    static {
       Restart var0 = new Restart();
       INSTANCE = var0;
    }
 }

文件外扩展子类可想而知。

public final class TestExtendedSealedClass extends GameAction {
    public TestExtendedSealedClass() {
       super(5, (DefaultConstructorMarker)null);
    }
 }

/   Sealed Interface   /

sealed interface 即密封接口,和 sealed class 有几乎一样的特点。比如:

限制接口的实现:一旦含有包含 sealed interface 的 module 经过了编译,就无法再有扩展的实现类了,即对其他 module 隐藏了接口。

还有些额外的优势:

帮助密封类、枚举类等类实现多继承和扩展性,比如搭配枚举,以处理更复杂的分类逻辑。

Additionally, sealed interfaces enable more flexible restricted class hierarchies because a class can directly inherit more than one sealed interface.

比如 Flappy Bird 游戏的过程中会产生很多 Action 来触发数据的计算以推动 UI 刷新以及游戏的进程,Action 可以用 enum class 来管理。其中有些 Action 是关联的,有些则没有关联、不是同一层级。但是 enum class 默认扩展自 Enum 类,无法再嵌套 enum。

Enum class cannot inherit from classes

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

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 -> TODO()
 
         Action.Start -> TODO()
         Action.Exit -> TODO()
         Action.Restart -> TODO()
 
         Action.Up -> TODO()
         Action.Down -> TODO()
         Action.HitGround -> TODO()
         Action.HitPipe -> TODO()
         Action.CrossedPipe -> TODO()
 
         Action.Move -> TODO()
         Action.Reset -> TODO()
 
         Action.RoadMove -> TODO()
         Action.RoadReset -> TODO()
     }
 }

借助 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 -> TODO()

         is GameAction -> {
             when (action) {
                 GameAction.Start -> TODO()
                 GameAction.Exit -> TODO()
                 GameAction.Restart -> TODO()
             }
         }
         is BirdAction -> {
             when (action) {
                 BirdAction.Up -> TODO()
                 BirdAction.Down -> TODO()
                 else -> TODO()
             }
         }
         is PipeAction -> {
             when (action) {
                 PipeAction.Move -> TODO()
                 PipeAction.Reset -> TODO()
             }
         }
         is RoadAction -> {
             when (action) {
                 RoadAction.Move -> TODO()
                 RoadAction.Reset -> TODO()
             }
         }
     }
 }

/   总结   /

Sealed Class & Interface VS Enum

总体来说 sealed class 和 interface 和 enum 有相近的地方,也有明显区别,需要留意:

  • 每个 enum 常量只能以单例的形式存在

  • sealed class 子类可以拥有多个实例,不受限制,每个均可以拥有自己的状态

  • enum class 不能扩展自 sealed class 以及其他任何 Class,但他们可以实现 sealed 等 interface


Sealed Class VS Interface

Sealed classes and interfaces represent restricted class hierarchies that provide more control over inheritance.

sealed class 和 interface 都意味着受限的类层级结构,便于在继承和实现上进行更多控制。具备如下的共同特性:

其 sub class 需要定义在同一 Module 以及同一 package,不局限于 sealed 内部或同文件内。

看下对比:

ba37c0665a5e7cc01c65a826097af74d.jpeg

推荐阅读:

我的新书,《第一行代码 第3版》已出版!

Android Drawable妙用和mutate()源码解析

Kotlin Flow响应式编程,基础知识入门

欢迎关注我的公众号

学习技术或投稿

a1530c24a6571934948d2f2adc38692b.png

22f254f533ffcd5cc1bef47f1231e326.jpeg

长按上图,识别图中二维码即可关注

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值