这篇文章依旧没有什么主题、没有什么深度，纯碎随笔，谈的也是 Kotlin 最最基础的问题。我常常说 Kotlin 比 Swift 舒服的一点是，Kotlin 在 null-safety 问题上，会有所谓「smart cast」。例如你有var a: Foo? = getFoo() if (a != null) { print(a.foo) // 不再需要 !! } 而在 Swift 里，你仍然需要手动再次「!」var a: Foo? = getFoo() if (a != null) { print(a!.foo) // 仍然需要 ! } 当然所有人都会很快注意到的一点是，Kotlin 对于局部变量可以做这样的处理，但如果是个类的成员变量（var），就不行：class Bar { var a: Foo? = getFoo() fun bar() { if (a != null) { print(a.foo) // 编译无法通过 } } } 为什么 Kotlin 此时不予编译通过呢？因为在当前线程判断 a != null 之后，其它线程可能又修改了「a」的值。事实上，如果是用 val 声明的 field，此时就可以 smart cast。而如果是 var 声明的呢？如果你加上 !!，变成 a!!.foo，就可以编译通过，但这么做的话，你就会被抓住游街。所以，正确的解决方式是，定义一个局部变量，这样就确保没有其它线程能动到它。fun bar() { val localA = a if (localA != null) { print(localA.foo) // cool } } 这样写的唯一问题，就是丑。定义了一个可能只用到一次的局部变量，而它并不被整个方法所需要，而仅仅只是临时用到而已。大约 Swift 程序员们也意识到了这一点，所以 Swift 里习惯的 practice 是：（摘自官方教程）if let roomCount = john.residence?.numberOfRooms { print("John's residence has \(roomCount) room(s).") } else { print("Unable to retrieve the number of rooms.") } 也就是，为了写得更简明一些，Swift 从语法层面增加了 if let 语句解决问题。Kotlin 里，当然也可以利用 let 来写得漂亮些。当然，Kotlin 的 DSL 能力那么好，何必再从语法层面解决问题？print(john.residence?.numberOfRooms?.let { "John's residence has $it room(s)." } ?: "Unable to retrieve the number of rooms.") 一气呵成，不需要任何额外的语法。事实上，我实际写代码时，也经常会 foo?.let { … } 这么用。上面语句的核心就是 let。和 Swift 不同，Kotlin 里的 let 不过只是一个普通的函数而已。事实上，很多 Kotlin 教程都不会提及标准库里这几个很赞的函数。它们实现得非常简单，但却可以作为良好 Kotlin 代码的典范：public inline fun <T, R> T.run(block: T.() -> R): R = block() public inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block() public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this } public inline fun <T> T.also(block: (T) -> Unit): T { block(this); return this } public inline fun <T, R> T.let(block: (T) -> R): R = block(this) public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? = if (predicate(this)) this else null 熟悉它们，能够帮你大大地缩短代码。例如某个方法，在计算出返回值后，还需要做一些清理，之前你可能：fun foo(): Int { val resource = getResource() val retVal = resource.calculate() resource.cleanUp() return retVal } 而现在，仅靠标准库里的这几个方法，你就可以：fun foo() = getResource().run { calculate().also { cleanUp() } } 标准库里的这几个方法仅仅是最最常用和最简单的对控制语句的补充。其实还有很多这样的扩展方法可以用，例如 funKTionale 里面的函数，都很棒呢。此文同时发布于 https://zhuanlan.zhihu.com/p/27073170。
Data class 是好东西，Kotlin 就差一个 pattern matching 了
我注意到在我前些天的一篇回答的评论里，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 这两个东西，用在一起真是蛮合适呢。其实这个例子里，不加 sealed 和 data 两个修饰符，也能编译通过。但是，加上这两个修饰符，似乎更能表达类似于 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。