Kotlin 与 DSL
DSL构建
前置知识
- Kotlin 契约(Contracts):建议先完成前一篇的学习
学习目标
- 掌握「概述」的核心机制、典型用法与常见陷阱
- 掌握「基础概念」的核心机制、典型用法与常见陷阱
- 掌握「快速上手」的核心机制、典型用法与常见陷阱
- 掌握「详细用法」的核心机制、典型用法与常见陷阱
- 掌握「常见场景」的核心机制、典型用法与常见陷阱
类型安全构建器
// 用泛型和密封类确保类型安全
sealed class Node {
data class Element(val tag: String, val children: List<Node>, val attrs: Map<String, String>) : Node()
data class Text(val content: String) : Node()
}
class ElementBuilder(val tag: String) {
private val children = mutableListOf<Node>()
private val attrs = mutableMapOf<String, String>()
operator fun String.unaryPlus() {
children.add(Node.Text(this))
}
fun attr(name: String, value: String) {
attrs[name] = value
}
fun element(tag: String, block: ElementBuilder.() -> Unit = {}) {
val builder = ElementBuilder(tag)
builder.block()
children.add(Node.Element(tag, builder.children, builder.attrs))
}
fun build(): Node.Element = Node.Element(tag, children, attrs)
}
fun element(tag: String, block: ElementBuilder.() -> Unit = {}): Node.Element {
val builder = ElementBuilder(tag)
builder.block()
return builder.build()
}
fun main() {
val doc = element("div") {
attr("class", "container")
element("h1") { +"标题" }
element("p") { +"段落内容" }
}
println(doc)
}
概述
DSL(Domain-Specific Language,领域特定语言)是针对特定领域设计的专用语言。Kotlin 的扩展函数、带接收者的 lambda、中缀函数等特性,使得在 Kotlin 中构建类型安全的 DSL 非常方便。你已经在很多地方见过 Kotlin DSL:Gradle 构建脚本、Ktor 路由定义、Compose UI 声明等都是 DSL 的典型应用。
理解 DSL 的构建原理,不仅能让你更好地使用这些框架,还能让你为自己的业务领域创建专用的 DSL,提升代码的可读性和表达力。
基础概念
- 带接收者的 lambda:
Type.() -> Unit,lambda 内部的this指向接收者对象 - 构建器模式(Builder Pattern):DSL 通常基于构建器模式,通过链式调用构建复杂对象
- 作用域控制:通过不同的接收者类型,限制 lambda 内部可以调用的方法
- 中缀函数:
infix关键字修饰的函数,调用时可以省略点号和括号
快速上手
一个最简单的 DSL:
// 定义构建器
class HtmlBuilder {
private val content = StringBuilder()
fun body(block: BodyBuilder.() -> Unit) {
val builder = BodyBuilder()
builder.block()
content.append("<body>${builder.build()}</body>")
}
fun build(): String = "<html>${content}</html>"
}
class BodyBuilder {
private val content = StringBuilder()
fun p(text: String) {
content.append("<p>$text</p>")
}
fun h1(text: String) {
content.append("<h1>$text</h1>")
}
fun build(): String = content.toString()
}
// 顶层函数,DSL 入口
fun html(block: HtmlBuilder.() -> Unit): String {
val builder = HtmlBuilder()
builder.block()
return builder.build()
}
// 使用 DSL
fun main() {
val result = html {
body {
h1("标题")
p("段落内容")
}
}
println(result)
// <html><body><h1>标题</h1><p>段落内容</p></body></html>
}
详细用法
带接收者的 lambda
这是 DSL 的核心机制:
class TableBuilder {
private val rows = mutableListOf<String>()
// 带接收者的 lambda:在 lambda 内部 this 指向 RowBuilder
fun row(block: RowBuilder.() -> Unit) {
val builder = RowBuilder()
builder.block() // 执行 lambda,this 是 builder
rows.add(builder.build())
}
fun build(): String = rows.joinToString("\n", "<table>\n", "\n</table>")
}
class RowBuilder {
private val cells = mutableListOf<String>()
fun cell(text: String) {
cells.add("<td>$text</td>")
}
fun build(): String = cells.joinToString("", "<tr>", "</tr>")
}
fun table(block: TableBuilder.() -> Unit): String {
val builder = TableBuilder()
builder.block()
return builder.build()
}
fun main() {
val html = table {
row {
cell("姓名")
cell("年龄")
}
row {
cell("Alice")
cell("25")
}
}
println(html)
}
中缀函数增强 DSL
class SqlBuilder {
private val parts = mutableListOf<String>()
fun select(vararg columns: String) {
parts.add("SELECT ${columns.joinToString(", ")}")
}
// 中缀函数让语法更自然
infix fun from(table: String) {
parts.add("FROM $table")
}
infix fun where(condition: String) {
parts.add("WHERE $condition")
}
infix fun orderBy(column: String) {
parts.add("ORDER BY $column")
}
fun build(): String = parts.joinToString(" ")
}
fun sql(block: SqlBuilder.() -> Unit): String {
val builder = SqlBuilder()
builder.block()
return builder.build()
}
fun main() {
val query = sql {
select("name", "age", "email")
from "users"
where "age > 18"
orderBy "name"
}
println(query)
// SELECT name, age, email FROM users WHERE age > 18 ORDER BY name
}
作用域控制
通过不同的接收者类型,限制 lambda 内部可用的方法:
// 只在配置阶段可用的方法
@DslMarker
annotation class ConfigDsl
@ConfigDsl
class ServerConfig {
var host: String = "localhost"
var port: Int = 8080
fun database(block: DatabaseConfig.() -> Unit) {
val config = DatabaseConfig()
config.block()
}
}
@ConfigDsl
class DatabaseConfig {
var url: String = ""
var username: String = ""
var password: String = ""
}
// 使用 @DslMarker 防止在嵌套 lambda 中意外调用外层方法
fun serverConfig(block: ServerConfig.() -> Unit): ServerConfig {
val config = ServerConfig()
config.block()
return config
}
fun main() {
val config = serverConfig {
host = "0.0.0.0"
port = 9090
database {
url = "jdbc:postgresql://localhost:5432/mydb"
username = "admin"
// 这里不能直接调用 host 或 port
// 因为 @DslMarker 限制了作用域
}
}
}
泛型 DSL
class TreeNode<T>(val value: T) {
private val children = mutableListOf<TreeNode<T>>()
fun child(value: T, block: TreeNode<T>.() -> Unit = {}) {
val node = TreeNode(value)
node.block()
children.add(node)
}
fun print(indent: Int = 0) {
println(" ".repeat(indent) + value.toString())
children.forEach { it.print(indent + 1) }
}
}
fun <T> tree(root: T, block: TreeNode<T>.() -> Unit = {}): TreeNode<T> {
val node = TreeNode(root)
node.block()
return node
}
fun main() {
val org = tree("CEO") {
child("CTO") {
child("开发经理")
child("测试经理")
}
child("CFO") {
child("财务经理")
}
}
org.print()
// CEO
// CTO
// 开发经理
// 测试经理
// CFO
// 财务经理
}
常见场景
配置 DSL
class HttpClientConfig {
var baseUrl: String = ""
var timeout: Int = 30000
var retryCount: Int = 0
private val headers = mutableMapOf<String, String>()
fun header(name: String, value: String) {
headers[name] = value
}
fun headers(): Map<String, String> = headers.toMap()
}
fun httpClient(block: HttpClientConfig.() -> Unit): HttpClientConfig {
val config = HttpClientConfig()
config.block()
return config
}
fun main() {
val config = httpClient {
baseUrl = "https://api.example.com"
timeout = 10000
retryCount = 3
header("Authorization", "Bearer token123")
header("Accept", "application/json")
}
println("连接 ${config.baseUrl},超时 ${config.timeout}ms")
}
测试 DSL
class TestCase(val name: String) {
private val steps = mutableListOf<String>()
private var setup: (() -> Unit)? = null
private var teardown: (() -> Unit)? = null
fun setup(block: () -> Unit) { setup = block }
fun teardown(block: () -> Unit) { teardown = block }
fun step(description: String, block: () -> Unit) {
steps.add(description)
block()
}
fun run() {
println("测试: $name")
setup?.invoke()
steps.forEach { println(" - $it") }
teardown?.invoke()
println("通过")
}
}
fun test(name: String, block: TestCase.() -> Unit) {
val testCase = TestCase(name)
testCase.block()
testCase.run()
}
fun main() {
test("用户登录") {
setup { println(" 准备测试数据") }
step("打开登录页面") { /* ... */ }
step("输入用户名和密码") { /* ... */ }
step("点击登录按钮") { /* ... */ }
step("验证跳转到首页") { /* ... */ }
teardown { println(" 清理测试数据") }
}
}
报告生成 DSL
class ReportBuilder {
private val sections = mutableListOf<String>()
var title: String = ""
fun section(name: String, block: SectionBuilder.() -> Unit) {
val builder = SectionBuilder(name)
builder.block()
sections.add(builder.build())
}
fun build(): String = "# $title\n\n${sections.joinToString("\n\n")}"
}
class SectionBuilder(val name: String) {
private val items = mutableListOf<String>()
fun item(text: String) { items.add("- $text") }
fun code(text: String) { items.add("```\n$text\n```") }
fun build(): String = "## $name\n${items.joinToString("\n")}"
}
fun report(block: ReportBuilder.() -> Unit): String {
val builder = ReportBuilder()
builder.block()
return builder.build()
}
fun main() {
val text = report {
title = "项目周报"
section("本周进展") {
item("完成用户模块开发")
item("修复3个线上Bug")
}
section("下周计划") {
item("开始订单模块开发")
item("性能优化")
}
}
println(text)
}
注意事项
- 使用 @DslMarker 防止作用域泄漏:没有 @DslMarker,嵌套 lambda 中可以意外调用外层的方法
- DSL 函数应该简洁:每个 DSL 函数只做一件事,保持单一职责
- 避免在 DSL 中使用 return:lambda 中的 return 会从外层函数返回,使用 return@label 或避免
- DSL 不适合复杂逻辑:DSL 用于声明式配置,复杂控制流应该用普通代码
- 保持不可变性:构建完成后,结果应该是不可变的
进阶用法
动态属性
class AttributeBuilder {
private val attrs = mutableMapOf<String, String>()
// 支持动态属性名
operator fun set(key: String, value: String) {
attrs[key] = value
}
fun build(): String = attrs.entries
.joinToString(" ") { """${it.key}="${it.value}"""" }
}
fun main() {
val attrs = AttributeBuilder().apply {
this["class"] = "container"
this["id"] = "main"
this["data-role"] = "content"
}
println(attrs.build())
// class="container" id="main" data-role="content"
}
DSL 基础
基本写法:带接收者的 Lambda
fun <函数名>(<参数>, <块>: <接收者类型>.() -> Unit)
// 定义 DSL 入口函数
fun html(block: Html.() -> Unit): Html {
return Html().apply(block)
}
基本写法:构建器类
class <类名> { fun <方法>(<参数>, <块>: <类型>.() -> Unit) }
// 构建器模式
class Html {
private val elements = mutableListOf<String>()
fun body(block: Body.() -> Unit) {
elements.add(Body().apply(block).toString())
}
}
基本写法:@DslMarker 限制作用域
@DslMarker annotation class <名称>
// 防止外部接收者访问
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class Body { fun p(text: String) { } }
基本写法:invoke 约定
operator fun <函数名>.invoke(<参数>): <返回类型>
// 让对象像函数一样调用
class Config {
operator fun invoke(name: String, value: String) {
/* 设置配置 */
}
}
中缀调用
基本写法:infix 中缀函数
infix fun <函数名>(<参数>): <返回类型>
// 定义中缀函数
infix fun Int.toPower(exp: Int): Int = Math.pow(this.toDouble(), exp.toDouble()).toInt()
val result = 2 toPower 10
基本写法:to 配对
<值1> to <值2>
// 创建 Pair
val pair = "key" to "value"
基本写法:区间操作
<开始> .. <结束>
// 创建区间
for (i in 1..10) { }
val range = 1 until 100
属性委托
基本写法:lazy 懒加载
val <变量>: <类型> by lazy { <初始化> }
// 懒加载属性
val expensive by lazy { computeExpensive() }
基本写法:observable 可观察
var <变量>: <类型> by Delegates.observable(<初始值>) { <属性>, <旧值>, <新值> -> }
// 监听属性变化
var name by Delegates.observable("") { _, old, new ->
println("$old -> $new")
}
基本写法:vetoable 可否决
var <变量>: <类型> by Delegates.vetoable(<初始值>) { <属性>, <旧值>, <新值> -> <是否允许> }
// 可否决属性变化
var age by Delegates.vetoable(0) { _, _, new -> new >= 0 }
基本写法:map 委托
val <变量>: <类型> by <map>
// 从 Map 委托属性
class User(map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}
自定义委托
基本写法:ReadOnlyProperty 只读委托
class <类名> : ReadOnlyProperty<<所有者>, <类型>> { override fun getValue(...) }
// 自定义只读委托
class TrimDelegate : ReadOnlyProperty<Any?, String> {
override fun getValue(thisRef: Any?, property: KProperty<*>): String {
return property.name.trim()
}
}
基本写法:ReadWriteProperty 读写委托
class <类名> : ReadWriteProperty<<所有者>, <类型>> { ... }
// 自定义读写委托
class Counter : ReadWriteProperty<Any?, Int> {
private var value = 0
override fun getValue(thisRef: Any?, property: KProperty<*>): Int = value
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
this.value = value
}
}