相信近年来,也许有这么一批人会认为 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))
})
}
}