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

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

前言

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


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


Sealed Class

Sealed Interface

Sealed Class & Interface VS Enum

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

总结

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

参考资料

https://kotlinlang.org/docs/sealed-classes.html

https://kotlinlang.org/docs/whatsnew15.html#sealed-interfaces

https://blog.csdn.net/vitaviva/article/details/117938161

https://jorgecastillo.dev/sealed-interfaces-kotlin

https://www.rockandnull.com/kotlin-sealed-interface/


相关文章
|
2月前
|
存储 Java 编译器
Kotlin学习教程(八)
Kotlin学习教程(八)
|
1月前
|
Java Kotlin
Kotlin学习教程(七)
《Kotlin学习教程(七)》主要介绍了Lambda表达式,这是一种匿名函数,广泛用于简化代码。文章通过与Java 8 Lambda表达式的对比,展示了Kotlin中Lambda的基本语法、参数声明、函数体定义及如何作为参数传递。示例包括按钮事件处理和字符串比较,突出了Lambda表达式的简洁性和实用性。
38 4
|
2月前
|
Java Maven Kotlin
vertx的学习总结7之用kotlin 与vertx搞一个简单的http
本文介绍了如何使用Kotlin和Vert.x创建一个简单的HTTP服务器,包括设置路由、处理GET和POST请求,以及如何使用HTML表单发送数据。
42 2
vertx的学习总结7之用kotlin 与vertx搞一个简单的http
|
2月前
|
Java Kotlin 索引
Kotlin学习教程(三)
Kotlin学习教程(三)
19 4
|
2月前
|
Java Kotlin
Kotlin学习教程(二)
Kotlin学习教程(二)
40 4
|
2月前
|
安全 Java 编译器
Kotlin学习教程(一)
Kotlin学习教程(一)
40 4
|
2月前
|
存储 Java API
Kotlin学习教程(六)
《Kotlin学习教程(六)》介绍了Kotlin中的注解、反射、扩展函数及属性等内容。注解用于添加元数据,反射支持运行时自省,扩展则允许为现有类添加新功能,无需修改原类。本文还详细解释了静态扩展的使用方法,展示了如何通过companion object定义静态部分,并对其进行扩展。
20 2
|
2月前
|
存储 设计模式 JSON
Kotlin学习教程(五)
《Kotlin学习教程(五)》介绍了Kotlin中的泛型、嵌套类、内部类、匿名内部类、枚举、密封类、异常处理、对象、单例、对象表达式、伴生对象、委托等高级特性。具体内容包括泛型的定义和类型擦除、嵌套类和内部类的区别、匿名内部类的创建、枚举类的使用、密封类的声明和用途、异常处理机制、对象和单例的实现、对象表达式的应用、伴生对象的作用以及类委托和属性委托的使用方法。通过这些内容,读者可以深入理解Kotlin的高级特性和设计模式。
23 1
|
2月前
|
Java Kotlin
Kotlin学习教程(十)
Kotlin学习教程(十)
|
2月前
|
XML 编译器 Android开发
Kotlin学习教程(九)
Kotlin学习教程(九)