前语

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 还具有如下特色或约束:

  1. sealed class 是笼统类,能够具有笼统方法,无法直接实例化。不然,编译器将提示如下:

    Sealed types cannot be instantiated

  2. sealed class 的结构函数只能具有两种可见性:默认情况下是 protected,还能够指定成 private,public 是不被答应的。

    Constructor must be private or protected in sealed class

  3. sealed class 子类可扩展部分以及匿名类以外的恣意类型子类,包括一般 class、data classobject、sealed class 等,子类信息在编译期可知。

    假使匿名类扩展自 sealed class 的话,会弹出过错提示:

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

  4. 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 内部集成了特点的 gettoString 以及 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 Actionenum 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()
           }
         }
       }
    }
    

总结

1. Sealed Class & Interface VS Enum

整体来说 sealed class 和 interface 和 enum 有附近的当地,也有明显差异,需求留心:

  • 每个 enum 常量只能以单例的形式存在
  • sealed class 子类能够具有多个实例,不受约束,每个均能够具有自己的状况
  • enum class 不能扩展自 sealed class 以及其他任何 Class,但他们能够完成 sealed 等 interface

2. 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 内部或同文件内

看下比照:

Sealed 适用/优势 原理
Class 约束类的扩展 abstract class
Interface 约束接口的完成 协助类完成多承继和复杂的扩展性 interface

参考资料

  • kotlinlang.org/docs/sealed…
  • kotlinlang.org/docs/whatsn…
  • blog.csdn.net/vitaviva/ar…
  • jorgecastillo.dev/sealed-inte…
  • www.rockandnull.com/kotlin-seal…