注册

深入学习 Kotlin 特色之 Sealed Class 和 Interface

前言

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

🤔 总结

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


作者:TechMerger
链接:https://juejin.cn/post/7160111185201725476
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册