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

703 阅读3分钟

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

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

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

示例(Kotlin):

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 代码

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版)

欢迎关注微信公众号:非也缘也