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

DSL 的全称是领域特定语言(Domain Specific Language),它是编程语言赋予开发者的一种特殊能力,通过它我们可以编写出一些看似脱离其原始语法结构的代码,从而构建出一种专有的语法结构。

在 Kotlin 中,实现 DSL 的方式并不固定。这里主要的学习目标是通过高阶函数的方式来实现 DSL,这也是 Kotlin 中实现 DSL 中最常见的方式。

在 Android 中,Gradle 是一种基于 Groovy 语言的构建工具,因此,当我们在 build.gradle 中添加依赖库时所编写的内容其实就是 Groovy 提供的 DSL 功能。下面借助 Kotlin 的 DSL,来实现类似的语法结构:

示例(Kotlin):

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
fun main() {
// 示例 1
// 通过返回值来获取所有添加的依赖
val libraries = dependencies {
implementation("com.squareup.retrofit2:retrofit:2.6.1")
implementation("com.squareup.retrofit2:converter-gson:2.6.1")
}
for (lib in libraries) {
println(lib)
}

// 打印结果:
// com.squareup.retrofit2:retrofit:2.6.1
// com.squareup.retrofit2:converter-gson:2.6.1
}

/**
* 示例 1:类似 build.gradle 中添加依赖库的语法结构
*/
class Dependency {
/**
* 使用集合来保存所有依赖库
*/
val libraries = ArrayList<String>()

fun implementation(lib:String){
libraries.add(lib)
}
}

/**
* 定义一个高阶函数
* 经过 DSL 设计后,可在项目中使用如下的语法结构:
* dependencies {
* implementation("androidx.recyclerview:recyclerview:1.1.0")
* }
*/
fun dependencies(block: Dependency.() -> Unit):List<String>{
val dependency = Dependency()
dependency.block()
return dependency.libraries
}

示例(Kotlin):动态生成表格所对应的 HTML 代码

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
fun main() {
// 示例 2:
val html = table {
tr {
td { "Apple" }
td { "Grape" }
td { "Orange" }
}
tr {
td { "Pear" }
td { "Banana" }
td { "Watermelon" }
}
}

// 在 DSL 中也可以使用 Kotlin 的其它语法特性。
val html1 = table {
repeat(2) {
tr {
val fruits = listOf("Apple", "Grape", "Orange")
for (fruit in fruits) {
td { fruit }
}
}
}
}

println(html)
println(html1)

// 打印结果:
// <table>
// <tr>
// <td>Apple</td>
// <td>Grape</td>
// <td>Orange</td>
// </tr>
// <tr>
// <td>Pear</td>
// <td>Banana</td>
// <td>Watermelon</td>
// </tr>
// </table>
// <table>
// <tr>
// <td>Apple</td>
// <td>Grape</td>
// <td>Orange</td>
// </tr>
// <tr>
// <td>Apple</td>
// <td>Grape</td>
// <td>Orange</td>
// </tr>
// </table>
}

/**
* 示例 2:动态生成表格所对应的 HTML 代码
*/
class Td {
// <td> 标签表示一个单元格,其中必然是要包含内容的。
var content = ""
// 返回一段 <td> 标签的 HTML 代码。
// 使用 \n 和 \t 转义符来换行和缩进是为了让输入的结果更直观,可以不加,
// 因为浏览器在解析 HTML 代码时是忽略换行和缩进的。
fun html() = "\n\t\t<td>$content</td>"
}

class Tr {
// <tr> 标签表示表格的行,它可以包含多个 <td>
private val children = ArrayList<Td>()
// 此函数接收一个定义到 Td 类中并且返回值是 String 的函数类型参数。
// 调用时,会创建 Td 对象,并调用其函数类型参数并获取它的返回值,然后赋值到 Td 类的 content 字段中,
// 这样就可以将调用 td() 时传入的 Lambda 表达式的返回值赋值给 content 字段了。
fun td(block: Td.() -> String) {
val td = Td()
td.content = td.block()
children.add(td)
}
// 返回一段 <tr> 标签的 HTML 代码,每个 Tr 可能包含多个 Td,因此用循环遍历集合,将所有 Td 都拼接到 Tr 中。
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()
}
}

// 接收一个定义到 Table 类中的函数类型参数,当调用 table() 时,
// 会先创建一个 Table 对象,接着调用函数类型参数,这样 Lambda 表达式中的代码就能得到执行,
// 最后调用 html() 获取生成的 HTML 代码,最为最终的返回值。
fun table(block: Table.() -> Unit): String {
val table = Table()
table.block()
return table.html()
}

备注

参考资料

第一行代码(第3版)