查看原文
其他

Kotlin 密封类进化了

ByteCode ByteCode 2022-12-14


这是 dhl 的第 34 篇原创文章


这是 Sealed Classes(密封类)系列的第三篇文章,之前的文章从原理、优化、实战以不同的角度分别介绍了  Sealed Classes 的强大。

Kotlin Sealed 是什么?为什么 Google 都在用 文章中主要包含以下内容:

  • Sealed Classes 原理分析?
  • 枚举、抽象类、Sealed Classes 分别有那些优缺点?
  • 分别在什么情况下使用枚举和 Sealed Classes?
  • 为什么 Sealed Classes 用于表示受限制的类层次结构?
  • 在项目中如何使用 Sealed Classes?
  • 禁止在 Sealed Classes 所定义的文件外使用, Kotlin 是如何做到的呢?
  • ......

Kotlin 中的密封类 优于 带标签的类 文章中主要包含以下内容:

  • 什么是 Tagged Classes(标记类)?
  • Tagged Classes 的优缺点,以及在项目中所带来的影响?
  • 如何使用 Sealed Classes 优化现有的代码,可以带来那些收益?

而这篇文章主要来介绍 Sealed Classes 在 Kotlin 1.5.0 上带来的新特性,在开始分析之前,先来简单的回顾一下之前文章的内容。

Sealed Classes

什么是 Sealed Classes?

Sealed Classes 用于表示受限制的类层次结构,其实这句话可以拆成两句话来理解。

  • Sealed Classes 用于表示层级关系: 子类可以是任意的类, 数据类、Kotlin 对象、普通的类,甚至也可以是另一个 Sealed
  • Sealed Classes 受限制: 必须在同一文件中,或者在 Sealed Classes 内部中使用,在 Kotlin 1.1 之前,规则更加严格,子类只能在 Sealed Classes 内部中使用

Sealed Classes 的优点:

  • Sealed Classes 使类之间的职责分明,提高代码的可读性
  • 扩展性强,可以在不修改原有的代码结构的基础上添加新的参数或者子类
  • 每个类中不包含无关的字段,在一定程度上减少对象所占用的内存
  • Sealed Classes 结合 when 表达式一起使用会更加的方便,when 语句下的所有分支可以通过快捷键 Mac/Win/Linux:Alt + Enter 自动生成,效果如下所示:

这里只是简单的回顾了一下之前的内容,接下来我们一起来看看,在新版本 Kotlin 1.5.0 中 Sealed Classes 给我们带来了那些优化。

Sealed Classes 进化了

在 2021 年 5 月份,Kotlin 官方发布了 1.5.0,在这个版本中带来几个非常有用的特性,而在这篇文章我们主要介绍 Sealed Classes,更多信息请查看 What's new in Kotlin 1.5.0
https://kotlinlang.org/docs/whatsnew15.html#kotlin-jvm

在 Kotlin 1.0 时,子类只能在 Sealed Classes 内部中使用,因为 Sealed class 会被编译成 abstract class,并且默认的构造方法被私有化了,所以子类必须嵌套在 Sealed Classes 类中。

在 Kotlin 1.1 时,允许顶级的 Sealed Classes 和它顶级子类在同一个文件中,因为编译器会自动生成了一个 公有 的构造方法,在子类的构造方法中调用了父类 公有 的构造方法,而这些都是 Kotlin 编译器帮我们做的。

在 Kotlin 1.5.0 中,放宽了对 Sealed Classes 限制,只需要保证 Sealed Classes 和它的子类,在同一个包名和 module 下面即可,这些都是  Kotlin 编译器帮我们做的。

接下里我们一起来分析一下为什么 Kotlin 需要升级 Sealed Classes?在之前的文章 Kotlin 中的密封类 优于 带标签的类 分析了 Sealed Classes 有很多优点,但是它也有很多不足之处,这也是为什么需要升级 Sealed Classes 的原因:

  • Sealed Classes 的子类,被限制在单个父类中
  • 把所有的代码放在一个文件中,会造成文件臃肿,可读性下降,如下所示
sealed class Color {
    class Red : Color()
    class Blue : Color()
    // ...... 更多的颜色
}

sealed class Figure {
    abstract fun draw()
}

class Rectangle(val color: Color) : Figure() {
    override fun draw() {
    }
}

class Round(val color: Color) : Figure() {
    override fun draw() {

    }
}

class Triangle(val color: Color) : Figure() {
    override fun draw() {

    }
}

...... // 随着需求的增加,文件会越发庞大

// 通过快捷键 Mac/Win/Linux:Alt + Enter 自动生成
fun drawFigure(figure: Figure) {
    when (figure) {
        is Rectangle -> TODO()
        is Round -> TODO()
        is Triangle -> when (figure.color) {
            is Color.Blue -> TODO()
            is Color.Red -> TODO()
        }
    }
}

在 Kotlin 1.1 中被限制在一个文件中,随着需求的增加,文件只会越发的庞大,现在放宽了限制之后,我们可以将子类拆分成单独的文件,可以进一步的提高代码的可读性。

  • 只允许顶级的 Sealed Classes 和它顶级的子类在同一个文件中,对于非顶级的 Sealed Classes,所有子类都应该在其内部声明,以下代码编译会失败

正如你所见 Sealed Classes 还是不够灵活,非顶级子类 Yellow 定在 Color 的外面,编译就会出错,而在 Koltin 1.5.0 放宽了限制之后,以上代码可以正常编译通过,不仅提高代码的可读性,而且灵活性也提高了。

除此之外还允许在 Sealed Classes 中声明受保护的构造函数。在 1.5.0 之前所有 Sealed Classes 的默认构造函数都是 private,但是在 1.5.0 之后默认构造函数都是 protected。

sealed class Figure {
    constructor() // 默认为 protected
    private constructor(area: Double) : this()
}

但是不允许声明为 public,因为编译器会自动生成,如果声明为 public 编译就会出错:

一起来看看声明为 protected 的构造函数,反编译后的 Java 代码都做了什么。Tools → Kotlin → Show Kotlin Bytecode

public abstract class Figure {
   private Figure() {
   }

   private Figure(double area) {
      this();
   }

   // $FF: synthetic method
   public Figure(DefaultConstructorMarker $constructor_marker) {
      this();
   }

   // $FF: synthetic method
   public Figure(double area, DefaultConstructorMarker $constructor_marker) {
      this(area);
   }
}

public final class Rectangle extends Figure {
   public Rectangle() {
      super(1.0D, (DefaultConstructorMarker)null);
   }
}

正如你所见,生成的构造方法还是私有的,只不过编译器会自动生成 公有 的构造方法,在子类的构造方法中调用了父类 公有 的构造方法。除此变化之外,另外还有一个重要的特性,增加了密封接口(Sealed Interface)。

注意:

如果你已经升级到 Kotlin 1.5.0,编译器还提示出错,请将下面的代码添加在 build.gradle 中。

compileKotlin {
    kotlinOptions {
        languageVersion = "1.5"
    }
}

Sealed Interface

Sealed Interface 和 Sealed Classes 一样都是用于表示受限制的类层次结构,所以它也拥有 Sealed Classes 所有的优点,Sealed Classes 拥有的特性 Sealed Interface 也都拥有。

但是不同之处在于 Sealed Classes 被限制在单个父类中,而 Sealed Interface 支持更灵活的受限制类层次结构,因为子类可以实现多个 Sealed Interface,如下所示。

// IColor.kt
sealed interface IColor
class Red : IColor
class Blue : IColor

// IArea.kt
sealed interface IArea {
    fun area(): Double
}

// IFigure.kt
sealed interface IFigure

class Round() : IFigure, IColor

class Rectangle(val length: Double, val width: Double) : IFigure, IArea, IColor {
    override fun area(): Double = length * width
}

Sealed Interface 允许子类有多个实现,自由度更高,使得代码更加的灵活,但是它和 Sealed Classes 一样,被限制在了同一个包名和 module 下面,如果违反这个限制编译就会出错。

关于 Sealed Interface 相关的内容,就先介绍到这里,这篇文章主要分析了 Sealed Classes 以及 Sealed Interface 优缺点,在 Kotlin 1.5.0 中还增加了其他的特性,将会在后续的文章中介绍。


全文到这里就结束了,如果有帮助欢迎 在看 、点赞 、分享  就是对我最大的鼓励

代码不止,文章不停

欢迎点击下方卡片关注我,持续分享最新的技术


推荐阅读



最后推荐我一直在更新维护的项目和网站:

  • 最新的 AndroidX Jetpack 相关组件的实战项目 以及 原理分析的文章
    https://github.com/hi-dhl/AndroidX-Jetpack-Practice

  • LeetCode / 剑指 offer / 国内外大厂面试题 / 多线程 题解,语言 Java 和 kotlin,包含多种解法、解题思路、时间复杂度、空间复杂度分析

    剑指 offer:https://offer.hi-dhl.com
    LeetCode:https://leetcode.hi-dhl.com

  • 最新 Android 10 源码分析系列文章
    https://github.com/hi-dhl/Android10-Source-Analysis

  • 一系列国外的技术文章,每篇文章都会有译者思考部分,对原文的更加深入的分析
    https://github.com/hi-dhl/Technical-Article-Translation

  • 「为互联网人而设计,国内国外名站导航」涵括新闻、体育、生活、娱乐、设计、产品、运营、前端开发、Android 开发等等网址
    https://site.51git.cn

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存