DSL的全称是领域特定语言(Domain Specific Language),它是编程语言赋予开发者的一种特殊能力,通过它我们可以编写出一些看似脱离其原始语法结构的代码,从而构建出一种专有的特殊结构。
Kotlin也是支持DSL的,并且在Kotlin中实现DSL的方式并不固定,比如infix函数构建出的特有语法结构就属于DSL。不过Kotlin中最常见的实现DSL方式是通过高阶函数。
其实DSL我们一直都在用,比如我们想要在项目中添加一些依赖库,需要在build.gradle文件中编写如下内容:

dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2"
}

Gradle是一种基于Groovy语言的构建工具,因此上述的语法结构其实就是Groovy提供的DSL功能。
借助Kotlin的DSL,我们也可以实现类似的语法结构。
首先新建一个DSL.kt文件,然后在里面实现定义一个Dependency类,代码如下所示

class Dependency {
    
    val libraries=ArrayList<String>()
    
    fun implementation(lib:String){
        libraries.add(lib)
    }
}

这里我们使用了一个List集合来保存所有的依赖库,然后又提供了一个implementation()方法,用于向List集合中添加依赖库
接下来定义一个dependencies高阶函数,代码如下所示

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

可以看到,dependencies函数接收一个函数类型参数,并且该参数是定义到Dependency类当中的,因此调用它的时候需要先创建一个Dependency的实例,然后再通过该实例调用函数类型参数,这样传入的Lambda表达式就能得到执行了。最后我们将Dependency类中保存的依赖库集合返回。
我们可以通过dependencies函数的返回值来获取所有添加的依赖库,代码如下所示:

fun main() {
    val libraries = dependencies {
        implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2")
        implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2")
    }
    for(lib in libraries){
        println(lib)
    }
}

这里用一个libraries变量接收dependencies函数的返回值,然后使用for -in循环将集合中的依赖库全部打印出来。

kotlin desktop kotlin desktopdsl_android


可以看到,已经成功将使用DSL语法结构添加的依赖库全部获取到了。

在前端开发中,网页的展示都是由浏览器解析HTML代码来实现的。HTML中定义了很多标签,其中< table>标签用于创建一个表格,< tr >标签用于创建表格的行,< td >标签用于创建单元格。将这3种标签嵌套使用,就可以定制出包含任意行列的表格了。

例如:

<table>
<tr>
<td>Apple</td>
<td>Grape</td>
<td>orange</td>
</tr>
<tr>
<td>Pear</td>
<td>Banana</td>
<td>Watermelon</td>
</tr>
</table>

这样就会创建一个两行三列的表格,修改后缀名,将文件名改为test.html然后双击文件,使用浏览器打开,显示如下:

kotlin desktop kotlin desktopdsl_html_02


如果有一个需求,要求我们在Kotlin中动态生成表格所对应的HTML代码,最直接的方式是字符串拼接,但是这种方式比较繁琐,而且代码难以阅读。

而借助DSL,我们可以使用一种方便的语法结构来动态生成表格所对应的HTML代码

定义一个Td类,代码如下

class Td{
    var content=""
    fun html()="\n\t\t<td>$content</td>"
}

由于< td >标签表示一个单元格,其中必然是要包含内容的,因此这里我们使用了一个content字段来存储单元格中显示的内容。另外,还提供了一个html()方法,当调用这个方法时就返回一段< td >标签的HTML代码,并将content中存储的内容拼接进去。为了让最终输出的结果更加直观,使用了\n和\t转义符来进行换行和缩进。
接下来定义一个Tr类,代码如下所示:

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()
    }
}

由于< tr >标签表示表格的行,它是可以包含多个< td >标签的,因此我们创建了一个children集合,用于存储当前Tr所包含的Td对象。接下来提供了一个td()函数,它接收一个定义到Td类中并且返回值是String的函数类型参数。 当调用td()函数时,会先创建一个Td对象,接着调用函数类型参数并获取它的返回值,然后赋值到Td类的content字段当中。这样就可以将调用td()函数时传入的Lambda表达式的返回值赋给content字段了。最后将创建了的td对象添加到children集合当中。
另外,Tr类中也定义了一个html()方法,它的作用和刚才Td类当中的html()方法一致。只是由于每个Tr都可能会包含很多个Td,因此我们需要使用循环来遍历children集合,将所有的子Td都拼接到< tr >标签中,从而返回一段嵌套的HTML代码。
定义好了Tr类之后,我们就可以使用如下语法格式来构建表格中的一行数据:

val tr=Tr()
tr.td{"Apple"}
tr.td{"Orange"}
tr.td{"Grape"}

接下来再定义一个Table类,代码如下

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()
    }
}

现在我们就可以使用如下语法结构来创建一个表格了:

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

还可以再进一步简化,定义一个table()函数,代码如下

fun table(block:Table.()->Unit):String{
    val table=Table()
    table.block()
    return table.html()
}

最后我们就可以使用如下语法结构动态生成一个表格所对应的HTML代码了:

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

kotlin desktop kotlin desktopdsl_html_03


另外,在DSL中也可以使用Kotlin的其他语法特性,比如通过循环来批量生成< tr >和< td >标签:

fun main() {
    val html = table {
        repeat(2){
            tr {
                val fruits= listOf("Apple","Grape","Orange")
                for(fruit in fruits){
                    td {
                        fruit
                    }
                }
            }
        }
    }
    println(html)
}

kotlin desktop kotlin desktopdsl_开发语言_04