我注意到在我前些天的一篇回答的评论里,Swift 和 Scala 都被拿来和 Kotlin 做比较。

我于是忽然想写一点没什么技术水平、也没什么内容结构的随笔,谈谈一些关于 data class 的一些有意思的地方。

我写 Swift 时也注意到,Swift 里的 enum 中每一项是可以有参数的,亦即「associated values」。例如:

enum Gender {
    case male
    case female
    case other(description: String)
}

于是写 Kotlin 的时候,我就在想,为什么 Kotlin 的 enum 不能带参数呢…… 后来我才意识到,其实「带 associated values 的 enum」实际上可以用 sealed class 的语法来等同:

sealed class Gender {
    object Male: Gender()
    object Female: Gender()
    data class Other(val description: String): Gender()
}

Sealed class 和 data class 这两个东西,用在一起真是蛮合适呢。其实这个例子里,不加 sealeddata 两个修饰符,也能编译通过。但是,加上这两个修饰符,似乎更能表达类似于 enum 的语义。为什么呢?

在 Kotlin 里,sealed class 也是为了协助 when 语句的 exhaustiveness check 的;也就是说,对于一个 sealed class,如果 when 已经处理了它的所有已知子类,那就不再需要 else 分支。事实上,Sealed class 这个概念在 Scala 里也有完全相同的存在——相似地,它的主要用途是协助 pattern matching 的 exhaustiveness check。

而 Scala 里也有着和 data class 相似的存在,那就是 case class。它们同样用于表示「仅仅用于组织数据的类」,也就是类似于「带了 tags 的 tuple」;并且,它们同样提供了符合直觉的 equals 方法的实现。

然而,在 Scala 里,正如「case class」这个名字所暗示,其一个很大的用途是在 pattern matching 中。在 Scala 中,假设有

case class Bar(noun: String, verb: String)

那么就可以对变量 foo: Bar 做

foo match {
    case Bar(_, "药丸") => "后面有药丸"
    case Bar("青果", predicate) => "青果" + predicate + "了"
    case _ => "啥都没有"
}

所以:Kotlin 不提供 pattern matching 简直就是反人类!

Improved Pattern Matching in Kotlin 提供了一些增强 when 语句的奇技淫巧。当然,这样的奇技淫巧的代价,就是丧失 exhaustiveness check 的功能——因为 when 语句的 exhaustiveness check 非常弱,仅仅能识别「is XXX」这样的条件。

利用类似的奇迹淫巧,再加上更肮脏的反射,我们就能对 Kotlin 的 data class 做类似的事情——至少实现上面那段 Scala 中那样,对 foo 中的属性的值做 matching。这里利用的是,Kotlin 对于 data class 会生成 componentN() 方法 。

「奇技淫巧」所需要的一点 bolterplating:

object any // 因为没法直接用 _

class MatchDataClass<T: Any>(private val kClass: KClass<T>, private val params: Array<out Any>) {
    init {
        if (!kClass.isData) { throw IllegalArgumentException("Not a data class!") }
    }
    operator fun contains(input: Any): Boolean { // 实现「in」操作符
        return if (!kClass.isInstance(input)) false else (
            params.mapIndexed { index, criteria ->
                (criteria is any) || (kClass.java.getMethod("component" + (index + 1)).invoke(input) == criteria)
            }.all { it }
        )
    }
}

inline fun <reified T: Any> match(vararg params: Any) = MatchDataClass(T::class, params)

于是,我们现在就可以写出跟上面的 Scala 代码有点像的 Kotlin 代码来:

when (foo) {
    in match<Bar>(any, "药丸") -> "后面有药丸"
    in match<Bar>("青果", any) -> { val (_, predicate) = foo; "青果" + predicate + "了" }
    else -> "啥都没有"
}

注意到,第二个 case 的代码比较恶心。这是因为 Scala 能够在 matching 的同时创建变量并赋值,而 Kotlin 再怎么 hack 也无法做到。幸而 Kotlin 还算支持 destructing。

当然,这样的写法有很多问题:

  • 用了反射,这样不仅慢,而且很脏
  • 编译器不会为你做类型检查,写出 match<Bar>(1, 2) 编译器也不会指出错误
  • 同样,exhaustiveness check 和 smart cast 就通通没法帮你了

所以,似乎我们还是应该期待 Kotlin 提供完整的 pattern matching 支持,才能发挥 sealed class 和 data class 的最大功力呢。

此文同时发布于 https://zhuanlan.zhihu.com/p/27050720

标签: scala, kotlin, swift

添加新评论