Kotlin 基础11 - 使用 DSL 构建专用的语法结构

使用 infix 函数构建更可读的语法

Kotlin 提供了一种高级语法糖特性:infix 函数。

infix 函数把编程语言调用的语法规则调整了一下,比如 A to B 这样的语法结构,实际上等价于 A.to(B) 的写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
infix fun String.beginsWith(prefix: String) = startsWith(prefix)
infix fun <T> Collection<T>.has(element: T) = contains(element)
infix fun <A, B> A.with(that: B): Pair<A, B> = Pair(this, that)

fun test() {
if ("Hello Kotlin" beginsWith "Hello") {
// TODO
}
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
if (list has "") {
// TODO
}

val map = mapOf("Apple" with 1, "Banana" with 2, "Orange" with 3)
}

mapOf() 函数实际上接收的是一个 Pair 类型的可变参数列表。而 to() 函数就是创建并返回了一个 Pair 对象。

1
2
3
4
5
6
7
/**
* Creates a tuple of type [Pair] from this and [that].
*
* This can be useful for creating [Map] literals with less noise, for example:
* @sample samples.collections.Maps.Instantiation.mapFromPairs
*/
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

使用定义泛型函数的方式将 to() 函数定义到了 A 类型下。并且接收一个 B 类型的参数,因此 A 和 B 可以是两种不同类型的泛型。

使用 DSL 构建专用的语法结构

领域特定语言是编程语言赋予开发者的一种特殊能力,通过它可以编写出一些看似脱离其原始语法结构的代码,从而构建出一种专有的语法结构。使用 infix 函数构建出的特有语法结构就属于 DSL 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Dependency {
val libraries = ArrayList<String>()
fun implementation(lib: String) {
libraries.add(lib)
}
}

fun dependencies(block: Dependency.() -> Unit): List<String> {
val dependency = Dependency()
dependency.block()
return dependency.libraries
}

fun main() {
val libraries = dependencies {
implementation("androidx.lifecycle:lifecycle-extensions:2.2.0")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.2.0")
}
for (lib in libraries) {
println(lib)
}
}

语法结构使用上和 build.gradle 文件中使用的语法结果并不完全相同,这主要是因为 Kotlin 和 Groovy 在语法层面还是有一定差别的。
这种语法结构比直接调用 Dependency 对象的 implementation() 方法要更加直观一些,需要添加的依赖库越多,使用 DSL 写法的优势就会越明显。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class Td {
var content = ""
fun html() = "\n\t\t<td>$content</td>"
}
class Tr {
private val children = ArrayList<Td>()
fun td(block: Td.() -> String) {
val td = Td()
td.content = td.block()
children.add(td)
}
fun html(): String {
val builder = StringBuilder()
builder.append("\n\t<tr>")
for (childTag in children) {
builder.append(childTag.html())
}
builder.append("\n\t</tr>")
return builder.toString()
}
}
class Table {
private val children = ArrayList<Tr>()
fun tr(block: Tr.() -> Unit) {
val tr = Tr()
tr.block()
children.add(tr)
}
fun html(): String {
val builder = StringBuilder()
builder.append("<table>")
for (childTag in children) {
builder.append(childTag.html())
}
builder.append("\n</table>")
return builder.toString()
}
}
fun table(block: Table.() -> Unit): String {
val table = Table()
table.block()
return table.html()
}
fun main() {

val html = table {
tr {
td { "Apple" }
td { "Grape" }
td { "Orange" }
}
tr {
td { "Pear" }
td { "Banana" }
td { "Watermelon" }
}
}
println(html)
}

DSL 中也可以使用 Kotlin 的其他语法特性。

1
2
3
4
5
6
7
8
9
10
val html = table {
repeat(2) {
tr {
val fruits = listOf("Apple", "Grape", "Orange")
for (fruit in fruits) {
td { fruit }
}
}
}
}