目录

Kotlin 中使用交叉类型与联合类型

之后文章会全部转到飞书文档发布,此网页可能存在更新不及时的问题,飞书文档首页 zsqw123 Homepage

Kotlin 中使用交叉类型与联合类型

强类型界著名的 Typescript 语言就支持相当丰富的类型操作符,如联合(Union)与交叉(Intersection),但 JVM 是没有的,所以 kotlin 也没做,但在编译器阶段是有做支持的,即编译器事实上处理了这种情况,但没有暴露给语言使用者。

交叉类型(Intersection Type)

比如在下面的例子中,kotlin 会将类型推断为 Comparable<*> & Serializable,这就是一种交叉类型。

1
val a = if (condition) 1 else ""

但这个例子对我们没什么大用,毕竟又不是得到了 Int & String 类型,得到的仅仅是他们的几个公共父类型的交叉,这对我们意义就变得小很多了。

但很显然本文一定会提出解决方案,不然写这篇文章就没什么意思了。

事情的转机出现在 kotlin 支持 context receiver 这一特性,通过它我们能让 this 隐式具有多个父类,这不就行交叉类型吗?于是就有了下面的扩展函数:

1
2
3
4
5
inline fun <V1, V2, R> with(v1: V1, v2: V2, block: context(V1, V2) () -> R) = block(v1, v2)

inline fun <V1, V2, V3, R> with(v1: V1, v2: V2, v3: V3, block: context(V1, V2, V3) () -> R) = block(v1, v2, v3)

inline fun <V1, V2, V3, V4, R> with(v1: V1, v2: V2, v3: V3, v4: V4, block: context(V1, V2, V3, V4) () -> R) = block(v1, v2, v3, v4)

常规的 Kotlin with 函数仅支持传入单个参数,不过在我们对其进行了一些魔改之后,其可以支持传入多个参数,并且 this 的类型也可以认为变成了 V1 & V2 & V3 这样的类型,因为我们通过 context 标记了 block 的 this 同时具备多层的隐式上下文,那么在使用处你就可以这么用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Foo(val foo: Int = 1)
class Bar(val bar: Int = 2)
class Baz(val baz: Int = 3)
class Qux(val qux: Int = 4)

fun main() {
    with(Foo(), Bar(), Baz(), Qux()) {
        println("$foo $bar $baz $qux")
    }
}

这样就能够直接通过 this 访问到 Foo、Bar、Baz、Qux 内部的成员 foo、bar、baz 和 qux,即我们得到了这四种类型的交叉。

不过如果你真的尝试了上面的代码,那么编译器一定会给你报错:

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f1b63905df484a5ab02974d1e4464bc5~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1416&h=152&s=32770&e=png&b=2b2f37

也有相关的 Youtrack:https://youtrack.jetbrains.com/issue/KT-54233 ,其原因是因为 Kotlin 目前还没有一种方式能确保 V1 和 V2 之间不存在继承关系,因为一旦他们存在继承关系的话,这里使用时就不知道应当使用哪一个上下文参数的方法了,编译器会很疑惑:究竟该使用 v1 还是 v2 的方法作为 this 呢?

不过依照目前编译器的实现(Kotlin 2.0)来说,是看顺序的,v1 会优先于 v2 被使用,在这里我会继续等待官方更合理的解法,不过对于我们要实现交叉类型这一点来说,我们可以简单一点,用一行代码就可以绕过这个错误:

1
@Suppress("SUBTYPING_BETWEEN_CONTEXT_RECEIVERS")

具体的代码可以移步 https://github.com/zsqw123/kt-little/blob/master/src/main/java/com/zsu/multipleWith.kt

不过,需要注意的是这种类型并不能被传播到 with 外部,交叉类型只能在 lambda 内部操作


此外,对于接口类型,泛型的 where 也可以实现类似的效果,这里不做展开。

联合类型(Union Type)

在 Kotlin 中使用联合类型需要付出抽象的成本,即使用 sealed class:

1
2
3
4
sealed interface Union

class Foo : Union
class Bar : Union

此时 Union 的类型即为 Foo | Bar,通过 sealed class 能够保证子类型只能为固定的几个类型。但 union type 的缺陷是其只能使用当前模块内定义的类,如果需要将其他模块定义的类(如第三方库或 stdlib 里面的类)就需要进行包装:

1
2
class Foo(content: String) : Union
class Bar(i: Int) : Union

这也就是上文所提到的付出“抽象的成本”,对于外部的类都必须做一层包装来达到伪 union,并且 Kotlin 也没有类似 Rust 的 Deref 自动解引用,调用时需要再调用一下其内部包装的属性,确实不优雅,但目前也只能这样去做。

1
2
3
4
when (union) {
    is Foo -> union.content
    is Bar -> union.i
}

此外,如果用了 value class,部分代码在部分场景下会得到一定的性能提升,能够减少一部分的抽象开销。

其它

此外,Kotlin 2.0 之后也有类似的打算来推进不同分类的 union type,详见:Kotlin 2.0 更新速览与 2.1、2.2+ 展望,但目前还是未实现的特性。