Kotlin 特色 sealed 关键字

sealed 意为密封的,可修饰类 class 和接口 interface,用来表示受限的继承结构。

Sealed Class

介绍

sealed class,密封类,密封类是一种特殊的抽象类,用于限制可以继承它的子类。

密封类具备最重要的一个特点:

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

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

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

  1. sealed class 是抽象类,可以拥有抽象方法,无法直接实例化。
  2. sealed class 的构造函数只能拥有两种可见性:默认情况下是 protected,还可以指定成 private,但不允许指定成 public。
  3. sealed class 子类可扩展局部以及匿名类以外的任意类型子类,包括普通 class、data classobject、sealed class 等,子类信息在编译期可知。
  4. sealed class 的实例,可配合 when 表达式进行判断,当所有类型覆盖后可以省略 else 分支。
  5. 当 sealed class 子类没有指定构造方法或定义任意属性的时候,建议定义成单例 object,因为即便实例化成多个实例,互相之间没有状态的区别。在 Kotlin 1.9 版本新增 data object 用于取代 object 的用法,编译器会提示"'sealed' sub-object can be converted to 'data object' ",toString() 不会打印 HashCode 等无用信息让输出更有意义。

使用

可以在 sealed class 内部定义子类:

sealed class Language {
    //定义在内部
    data object English : Language()
    data class French(val str: String) : Language()
    class German(str: String) : Language()
}

还可以在外部定义子类:

sealed class Language {
    //定义在内部
    data object English : Language()
    data class French(val str: String) : Language()
    class German(str: String) : Language()
}

//定义在同一文件中
data object Chinese : Language()
data class Japanese(val str: String) : Language()

此外还可以定义在同包名不同一文件下

//定义在同包名不同一文件下
data class Korean(val str: String) : Language()

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

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

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

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

  • 普通 class 类型的话则必须使用 is 关键字判断类型匹配

      fun demo(language: Language) {
          when (language) {
              Language.English -> {}
              is Language.French -> {}
              is Language.German -> {}
              Chinese -> {}
              is Japanese -> {}
              is Korean -> {}
          }
      }
    

我们知道Kotlin代码最终会编译成Java字节码的,让我们来看一下,上述的Kotlin代码反编译之后是怎么样的:

public abstract class Language {
    public /* synthetic */ Language(DefaultConstructorMarker defaultConstructorMarker) {
        this();
    }

    private Language() {
    }

    // subclass:data object
    public static final class English extends Language {
        public static final English INSTANCE = new English();

        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (!(obj instanceof English)) {
                return false;
            }
            English english = (English) obj;
            return true;
        }

        public int hashCode() {
            return -765073291;
        }

        public String toString() {
            return "English";
        }

        private English() {
            super(null);
        }
    }

    // subclass:data class
    public static final class French extends Language {
        private final String str;

        public static /* synthetic */ French copy$default(French french, String str2, int i, Object obj) {
            if ((i & 1) != 0) {
                str2 = french.str;
            }
            return french.copy(str2);
        }

        public final String component1() {
            return this.str;
        }

        public final French copy(String str2) {
            Intrinsics.checkNotNullParameter(str2, "str");
            return new French(str2);
        }

        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            return (obj instanceof French) && Intrinsics.areEqual(this.str, ((French) obj).str);
        }

        public int hashCode() {
            return this.str.hashCode();
        }

        public String toString() {
            return "French(str=" + this.str + ')';
        }

        public French(String str2) {
            super(null);
            Intrinsics.checkNotNullParameter(str2, "str");
            this.str = str2;
        }

        public final String getStr() {
            return this.str;
        }
    }

    // subclass:class
    public static final class German extends Language {
        public German(String str) {
            super(null);
            Intrinsics.checkNotNullParameter(str, "str");
        }
    }
}

可以看到 sealed class 本身被编译为 abstract class,其内部子类按类型有所不同:

  • data object 类型在 class 内部集成了静态的 INSTANCE 实例,同时还有equalstoString 以及 hashCode 函数。
  • data class 类型在 class 内部集成了属性的 getequalscopytoString 以及 hashCode 函数。
  • class 类型仍是普通的 class。

而外部子类和同包名不同一文件下的子类则自然是定义在 Language 抽象类外部,内容也是跟上面一样的。

Sealed Interface

sealed interface,密封接口,和 sealed class 有几乎一样的特点。此外,还有额外的优势,可以帮助密封类、枚举类等类实现多继承和扩展性,比如搭配枚举,以处理更复杂的分类逻辑。

举例:Flappy Bird 游戏的过程中会产生很多 Action 来触发数据的计算以推动 UI 刷新以及游戏的进程,Action 可以用 enum class 来管理。

其中有些 Action 是关联的,有些则没有关联、不是同一层级。但是 enum class 默认扩展自 Enum 类,无法再嵌套 enum。

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

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

借助 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 -> {}
         
         is GameAction -> {
             when (action) {
                 GameAction.Start -> {}
                 GameAction.Exit -> {}
                 GameAction.Restart -> {}
             }
         }
         is BirdAction -> {
             when (action) {
                 BirdAction.Up -> {}
                 BirdAction.Down -> {}
                 else -> {}
             }
         }
         is PipeAction -> {
             when (action) {
                 PipeAction.Move -> {}
                 PipeAction.Reset -> {}
             }
         }
         is RoadAction -> {
             when (action) {
                 RoadAction.Move -> {}
                 RoadAction.Reset -> {}
             }
         }
     }
 }

sealed 与 enum 的区别

  • enum:enum 只是一个值(常量),每个 enum 常量只能以单例的形式存在。
  • sealed:sealed可以是一个值(定义成 data object 不携带数据),还可以是一个有状态的值(定义成 data class 携带数据)。sealed class 子类可以拥有多个实例,不受限制,每个均可以拥有自己的状态。
  • enum class 不能扩展自 sealed class 以及其他任何 Class,但可以实现 sealed interface,正如上面 Action 的举例。
相关推荐
天下无贼!4 分钟前
2024年最新版TypeScript学习笔记——泛型、接口、枚举、自定义类型等知识点
前端·javascript·vue.js·笔记·学习·typescript·html
秋秋秋叶10 分钟前
Python学习——【2.3】for循环
python·学习
小白小白从不日白41 分钟前
react 高阶组件
前端·javascript·react.js
月夕花晨3741 小时前
C++学习笔记(30)
c++·笔记·学习
Mingyueyixi1 小时前
Flutter Spacer引发的The ParentDataWidget Expanded(flex: 1) 惨案
前端·flutter
Good_tea_h1 小时前
Android中的单例模式
android·单例模式
架构文摘JGWZ2 小时前
Kafka 消息丢失如何处理?
学习