相信近年来,也许有这么一批人会认为 Java 语法过于保守及传统,代码写起来就显得有点臃肿了,不直观也不方便,而相对于 Java 而言,Kotlin 虽然与 Java 同属 JVM 平台衍生出来的计算机语言,Kotlin 的语法却比起 Java 来讲有更大的语法自由度(这里仅从语法角度分析),因此我们得以很好地实现某些看起来更简洁更方便的写法,就如同我们今天要讲的主题:在 Kotlin 上让你的代码更加优雅。
开发环境
System:Ubuntu 18.04 IDE:IntelliJ IDEA 2018.2.5 JDK version:1.8.0_181(Java 8) Kotlin version:1.3.10(Kotlin 1.3)
DSL 是什么?
概念
阅读本篇文章要求读者应清晰地认识 DSL 的概念,以及 Kotlin Lambda 的思想。 关于 DSL 可参考我择写的另外一篇文章:
对于 Kotlin 而言,DSL 的思维究竟可以对代码有什么实际帮助?
如果你已经阅读过上面的文章,应该能够明白到 DSL 应在特定领域发挥作用的重要性,而在 Kotlin 上也是如此。如果你是使用 IntelliJ IDEA
作为你的 IDE,那么在你学习 Kotlin 的时候肯定会使用到内置的 Java to Kotlin converter(J2KC)
,也就是把复制后的 Java 的代码粘贴到 .kt
文件后自动转换成 Kotlin 代码,如下面 JavaFX 创建布局的示例:
原本的 Java 代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public void create() { Label label = new Label("This is label"); label.setStyle("-fx-font-weight: bold;"); label.setTextFill(Color.web("0069B1")); Rectangle rectangle = new Rectangle(46.0, 18.0); rectangle.setArcHeight(10.0); rectangle.setArcWidth(10.0); rectangle.setFill(Color.web("#CCEEFF")); rectangle.setPadding(new Insets(2.0, 3.0, 2.0, 3.0)); StackPane stack = new StackPane(); stack.setHgap(10); stack.setVgap(10); stack.children.addAll(rectangle, label); }
透过 J2KC
之后自动转换的 Kotlin 代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 fun create() { val label = Label("This is label") label.style("-fx-font-weight: bold;") label.textFill(Color.web("0069B1")) val rectangle = Rectangle(46.0, 18.0) rectangle.arcHeight(10.0) rectangle.arcWidth(10.0) rectangle.fill(Color.web("#CCEEFF")) rectangle.padding(Insets(2.0, 3.0, 2.0, 3.0)) val stack = StackPane() stack.hgap(10) stack.vgap(10) stack.children.addAll(rectangle, label) }
我们来回顾下上面代码的转化过,首先 J2KC
分别自动将函数标签从 public void create()
简化成了 fun create()
,而因为在 Java 里返回值为 void
而在 Kotlin 里的后置类型声明 fun create(): Unit
可以直接被简化掉。除此以外还有包含对局部变量类型声明直接简化成 var
val
以及在创建新实例时把 new
关键词直接去除。当然 J2KC
还有很聪明的一点,也就是能够识别出以 get
set
为开头的函数名,直接将其简化成像是对一个字段进行赋值一样,而且也将以 ;
为行结尾的符号也去掉了,看起来已经相当不错了。
美中不足
转换过程虽然简单,但也足够粗暴,我们再来观察一下转换后的结果,可以看到
1 2 3 4 val stack = StackPane() stack.hgap(10) stack.vgap(10) stack.children.addAll(rectangle, label)
stack.xxx
像这样的操作实在太繁琐,每设置一个值都得事先输入 stack.
,而且看上去代码也会显得特别密集,怎么办呢?这部分就是接下来便是我们要解决的问题了。
如何优化
善用 apply() 与 also() 函数
像是上面这种情况,我们可以透过 Kotlin 上一个叫 apply()
的函数解决!这个函数位于 Kotlin 标准库内的 Standard.kt
文件里,下面我直接把这一段源码贴上来:
1 2 3 4 5 6 7 8 9 10 11 /** * Calls the specified function [block] with `this` value as its receiver and returns `this` value. */ @kotlin.internal.InlineOnly public inline fun <T> T.apply(block: T.() -> Unit): T { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } block() return this }
源码分析
对于何为扩展函数与泛型本篇并不多作阐述,具体可参考官方文档: Kotlin - Extensions Kotlin - Generics
这个函数很简单,我们首先先看函数标签:public inline fun <T> T.apply(block: T.() -> Unit): T
,意思大概就是一个带有型参 T 内联函数 ,并以这个型参作为扩展函数 apply
的目标,并且接受一个 T.() -> Unit
的 Lambda 类型作为参数传入,最终返回型参实际值 T。可以看到其实最核心的部分其实是 T.() -> Unit
,也就是接受一个无参数无返回值的 Lambda,其作用就相当于是一个回调函数,而这个 Lambda 也是基于型参 T 扩展出来的函数。因此我们传入回调函数的时候就可以像这么写:example.apply {}
,在 {}
之内的内容就是我们回调函数需要执行的代码了。
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 val example = Example() example.apply { // 这里的 `this` 实际上指向的是 `example` 这个实例 this // 调用一条被定义在 `Example` 类下的函数实际上可以从 this.test() // 简化成这样,而类下方的字段则也是相同做法 test() }
实际使用
然后我们使用在之前提到的 JavaFX 例子上:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 fun create() { val label = Label("This is label").apply { style("-fx-font-weight: bold;") textFill(Color.web("0069B1")) } val rectangle = Rectangle(46.0, 18.0).apply { arcHeight(10.0) arcWidth(10.0) fill(Color.web("#CCEEFF")) padding(Insets(2.0, 3.0, 2.0, 3.0)) } val stack = StackPane().apply { hgap(10) vgap(10) children.addAll(rectangle, label) } }
感觉不够?我们还可以改成这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 fun create() { val label = val rectangle = val stack = StackPane().apply { hgap(10) vgap(10) children.addAll( Label("This is label").apply { style("-fx-font-weight: bold;") textFill(Color.web("0069B1")) }, Rectangle(46.0, 18.0).apply { arcHeight(10.0) arcWidth(10.0) fill(Color.web("#CCEEFF")) padding(Insets(2.0, 3.0, 2.0, 3.0)) }) } }