Kotlin 1.4 终于正式 release 了,作为其爱好者,肯定要第一时间享用了。在此之前已经试用了好久,从 Early Access 起,持续关注着,但是 EA 的版本无法用于生产也是众所周知的事情,好在现在终于可以起飞了。
对于 1.4 版本,我关注的改进如下:
- 接口 SAM 转换
- 混合命名和位置参数
- 可调用的引用改进
- 对 when 的改进
- Web全栈
- Kotlin/Multiplatform
- iOS Application
接口 SAM 转换本质上是对一个拥有 单个非默认抽象方法的接口
进行 lambda 转换,对于符合这个条件的接口,称之为 SAM Type
。在此之前,官方曾表示过,Kotlin 本身拥有高阶函数,不再需要 SAM 转换,然而在开发者强烈的呼声下,还是妥协了。
这个特性简单做个比较就一目了然:
Kotlin 1.3
interface Action<T> {
fun run(x: T)
}
fun<T> runAction(x: T, a: Action3<T>) = a.run(x)
fun main() {
runAction("2333", object: Action3<String> {
override fun run(x: String) {
println(x)
}
})
}
Kotlin 1.4
fun interface Action<T> {
fun run(x: T)
}
fun<T> runAction(x: T, a: Action<T>) = a.run(x)
fun main() {
runAction("2333") {
println(it)
}
}
混合命名和位置参数是又一项牛逼的改进,之前有一些讨论中,部分人认为它没有意义,但是实际上这是一个让 Kotlin 更适合用来描述科学计算的特性,还是来看个例子:
Kotlin 1.3
fun f(x: Int = 1, y: Int = 1) { ... }
fun main() {
f(1, y = 2) // 合法
f(x = 1, 2) // 不合法
}
Kotlin 1.4
fun f(x: Int = 1, y: Int = 1) { ... }
fun main() {
f(1, y = 2) // 合法
f(x = 1, 2) // 合法
}
Kotlin 1.4 还增加了对于函数调用能力的提升,具体的就引用几个官方的代码片段吧,一看就明白的那种:
1)不再需要做带有默认值的重载
Kotlin 1.3
fun foo(i: Int = 0): String = "$i!"
fun apply(func: () -> String): String = func()
fun applyInt(func: (Int) -> String): String = func(0)
fun main() {
println(apply(::foo)) // 不合法
println(applyInt(::foo)) // 合法
}
Kotlin 1.4
fun foo(i: Int = 0): String = "$i!"
fun apply(func: () -> String): String = func()
fun main() {
println(apply(::foo)) // 合法
}
2) 更强的方法引用
Kotlin 1.3
fun foo(f: () -> Unit) { ... }
fun returnsInt(): Int = 42
fun main() {
foo { returnsInt() } // 合法
foo(::returnsInt) // 不合法
}
Kotlin 1.4
fun foo(f: () -> Unit) { ... }
fun returnsInt(): Int = 42
fun main() {
foo { returnsInt() } // 合法
foo(::returnsInt) // 合法
}
3)支持重载 varargs 参数的方法调用
Kotlin 1.4
fun foo(x: Int, vararg y: String) { ... }
fun use0(f: (Int) -> Unit) {}
fun use1(f: (Int, String) -> Unit) {}
fun use2(f: (Int, String, String) -> Unit) {}
fun test() {
use0(::foo)
use1(::foo)
use2(::foo)
}
4) 支持类型引用的智能转换
Kotlin 1.4
sealed class Animal
class Cat : Animal() {
fun meow() {
println("meow")
}
}
class Dog : Animal() {
fun woof() {
println("woof")
}
}
fun perform(animal: Animal) {
val kFunction: KFunction<*> = when (animal) {
is Cat -> animal::meow
is Dog -> animal::woof
}
kFunction.call()
}
fun main() {
perform(Cat())
}
要说 Kotlin 1.4 另一个有用的改进,就是循环内的 when
了,一直以来,在循环场景下,when 内要写 break/continue 的的情况,就必须使用 label
,而 label 形如 goto,会给人以流程混乱的印象。Kotlin 1.4 的 when 终于可以自由的在循环里为所欲为了:
Kotlin 1.3
fun test(xs: List<Int>) {
LOOP@for (x in xs) {
when (x) {
2 -> continue@LOOP
17 -> break@LOOP
else -> println(x)
}
}
}
Kotlin 1.4
fun test(xs: List<Int>) {
for (x in xs) {
when (x) {
2 -> continue
17 -> break
else -> println(x)
}
}
}
好了,上面讲了那么多语法上的问题,下面才是重头戏,这次 Kotlin 1.4 带来了全新的项目向导,可以建立的项目种类非常之多。
其中最为人瞩目的就是 Web 全栈能力,下面也将详细讲述。
先来打开新的项目向导,来选中 Full-Stack Web Application
,在右侧可以看到项目的结构,即这个项目将会由公共部分,后端部分和前端部分组成。
点击下一步之后,会提示为每一个组成部分选择相应的组件,其中公共部分就是 Kotlin/Native
的代码,不能选择。后端部分固定为 Ktor
,可以选择要部署到的容器,如 Tomcat、Netty 或 Jetty,默认的选项是 Netty。而前端部分的渲染引擎选择就尤为重要了,从选项可以看出来,Kotlin/JS
目前已经与 React
有了深度的绑定,也就是说我们的前端部分是要用 React 来开发的。
建立完项目后,大致看一下代码结构就能明白它的工作原理了,先从后端代码开始看吧:
server.kt
fun HTML.index() {
head {
title("Hello from Ktor!")
}
body {
div {
+"Hello from Ktor"
}
div {
id = "root"
}
script(src = "/static/sample.js") {}
}
}
fun main() {
embeddedServer(Netty, port = 8080, host = "127.0.0.1") {
routing {
get("/") {
call.respondHtml(HttpStatusCode.OK, HTML::index)
}
static("/static") {
resources()
}
}
}.start(wait = true)
}
这段代码是一个标准的微服务,其中有一个路由指向了 /
,返回经由 HTML.index()
组装的页面,另一个路由指向了静态资源,即 /static
目录。
有这段代码,我们即可以顺利的跑起服务器,并且渲染出一个基本的页面。
那再来看前端代码,前端的入口是 client.kt
,其代码也很简单:
fun main() {
window.onload = {
render(document.getElementById("root")) {
child(Welcome::class) {
attrs {
name = "Kotlin/JS"
}
}
}
}
}
是不是和你们想得不一样,前端依然是 Kotlin 代码,而不是 JavaScript。这段代码的含义为,在 windows.onload
事件里,对页面上的 root 组件(即是后端代码里由 HTML.index() 组装的组件之一)进行渲染,渲染的内容为 Welcome
组件,并传入 name="Kotlin/JS"
作为参数。
Welcome
组件是一个标准的 React
组件,在 Kotlin/JS 内,它的写法是这样的:
external interface WelcomeProps : RProps {
var name: String
}
data class WelcomeState(val name: String) : RState
@JsExport
class Welcome(props: WelcomeProps) : RComponent<WelcomeProps, WelcomeState>(props) {
init {
state = WelcomeState(props.name)
}
override fun RBuilder.render() {
// 具体的渲染方案
}
}
注意此处的 @JsExport
注解,意为要将这个组件导出为 JS,如果没有这个注解,编译器将不会把组件进行导出,从而引起能编译但是运行起来报 JS 异常的问题。
最后讲一下公共部分代码的问题,很多写惯了 Kotlin/JVM 的同学,会上手就在这里写上很多 Java 里才有的东西,也会有部分人上手就在这里写 dynamic
,其实这些东西都不被支持。
理由也很简单,因为这个项目是两端的,一端在 JVM,一端在 JS,如果写了 JVM 的东西,那如何编译到 JS 去呢?反过来也是一样的,说白了就是不同平台所拥有的库不一样,光是有语言和语法,是没有办法跨平台编译的。
那是不是说公共代码就没有意义呢,其实也不是,公共代码提供的是一种更高层次的跨平台抽象,比如说我要实现 MD5 加密,这个算法在 JVM 和 JS 侧的实现是不一样的,所以我们可以这样做:
common.kt
expect fun String.md5(): String
server.kt
actual fun String.md5(): String = hash("MD5")
client.kt
external fun require(module: String): dynamic
val md5Impl = require("md5")
actual fun String.md5(): String = md5Impl(this) as String
在很早的时候,我就说过了在 Kotlin/Native
下,需要用 expect/actual 来指出预期
与 现实
,在这里再一次用到了。通过这种方式,就可以抽象出在 JVM/JS 同时可用的函数了。
在我们研究的过程中,有不止一个人问过我,这玩意怎么编译怎么运行啊,总不能永远 gradle run
吧,这样运维会炸的。然而用 gradle build
这种,曾经在 Ktor 项目是都好使的东西,怎么到了这就不好使了呢?
经过一番验证,我证实了这里确实有坑,官方在发布最新 Kotlin/Native 插件时,还是挖坑了。
简单来说,就是 gradle task 内,有一个 distZip
,这个 task 会从编译结果内选取它想要的东西然后打成 dist 包,坏就坏在这个文件选取上。
按 distZip
的规则,被选取的文件必须满足文件名的条件,即:
${project.name}-${project.version}.jar
然而在 Kotlin/Native 编译的过程中,该文件被命名为:
${project.name}-jvm-${project.version}.jar
因此 distZip
就找不到文件了,所以打出来的 dist 包永远都不能运行。
那么要修复这个问题,只需要在 distZip
之前,把文件名改了,让它能找到就行,这种修改一下编译脚本就好了:
jvmJar.doLast {
copy {
from("build/libs/${project.name}-jvm-${project.version}.jar")
into('build/libs/')
rename{ "${project.name}-${project.version}.jar" }
}
}
distZip {
into("${project.name}-${project.version}") {
from '.'
include 'lib*.*'
}
}
现在我们就可以通过 gradle distZip
命令,来得到一个可部署的程序包,不再需要依赖 gradle 了。当然对于某些需求场景,也可以直接使用 gradle installDist
命令来直接部署。
本次 Kotlin 1.4 对跨平台的改进也很不错,终于从要设置一堆的 target 改成了对单一平台的编译,似乎是向 CodeTyphon 靠拢了,而且编译脚本的写法也简单了许多。
例如以下配置:
kotlin {
def hostOs = System.getProperty("os.name")
def isMingwX64 = hostOs.startsWith("Windows")
KotlinNativeTargetWithTests nativeTarget
if (hostOs == "Mac OS X") nativeTarget = macosX64('native')
else if (hostOs == "Linux") nativeTarget = linuxX64("native")
else if (isMingwX64) nativeTarget = mingwX64("native")
else throw new GradleException("Host OS is not supported in Kotlin/Native.")
nativeTarget.with {
binaries {
executable {
entryPoint = 'main'
}
}
}
sourceSets {
nativeMain {
}
nativeTest {
}
}
}
这次的更新,使得 Kotlin/Multiplatform 不再需要去寻找对应平台的依赖和相应的 posix,插件会自动加载全部的依赖,比以前方便太多了。
Kotlin/Multiplatform 当前的不足之处在于,编译时对内存的要求实在是高,按默认配置来编译居然会经常出现 Out of Memory
,而且我编译的程序是只有几行代码的 Sample。另外,由于对跨平台机制的重新设计,使得原本跨树莓派,跨 WebAssembly 等能力丢失,而且目前要加载一个第三方的 C/C++ 库依然是一个非常头疼的问题。
针对编译 OOM 的情况,解决方案是给予 gradle 更大的内存,以避免这样的情况发生:
org.gradle.parallel=true
org.gradle.daemon=true
org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError
在将内存改大后,可以明显发现编译变快,但是 OOM 的情况依然时有出现,只能说这东西对内存实在太苛刻了。
最后说一下 iOS App 的开发吧,这次更新的项目向导已经可以建立 iOS App 了,但是割裂感很重,主要体现在,没有办法直接引用 shared library,没有办法编辑 storyboard,也没有 xcode 那样的界面预览,在开发完 shared library 后,依然需要去 xcode 进行编译和调试,可以跨平台但是没有 IDE 的支持,实质上造成的反而是开发成本的提高。至少目前我还不会使用 Kotlin/Native 来开发 iOS App,那太蛋疼了。
总的来说,Kotlin 1.4 的更新在很大程度上方便了开发,我指的是 JVM/JS,也让我更加愿意在前端上花费一些力气,比如说再多学一下 React,但是对于跨平台端,原生端,或许还真的不到时候,还有很长的路要走。