Kotlin 语言 DSL 学习
DSL(Domain Specific Language,领域特定语言)是 Kotlin 中非常强大的特性,它允许你创建专门针对特定领域的简洁、表达力强的代码。
1. 什么是 DSL?
基本概念
DSL 是针对特定领域的专用语言,相比通用编程语言,它更简洁、更具表达力。
// 非 DSL 方式
val person = Person("John", 25, "New York")
// DSL 方式
val person = person {
name = "John"
age = 25
city = "New York"
}
Kotlin DSL 的核心特性
- 扩展函数
- 中缀调用
- Lambda 表达式作为最后一个参数
- 带接收者的 Lambda 表达式
2. DSL 的基础构建块
带接收者的 Lambda 表达式
这是 DSL 的核心技术:
// 带接收者的 Lambda
class HtmlBuilder {
fun body(init: BodyBuilder.() -> Unit) {
val bodyBuilder = BodyBuilder()
bodyBuilder.init()
// 处理构建结果
}
}
class BodyBuilder {
fun p(text: String) {
// 添加段落
}
}
// 使用
val html = HtmlBuilder()
html.body {
p("Hello") // 这里的 this 是 BodyBuilder 实例
p("World")
}
中缀函数
infix fun String.should(assertion: Assertion): AssertionResult {
return assertion.test(this)
}
// 使用
"hello" should startWith("h")
3. 创建简单的 DSL
示例 1:配置构建器
class DatabaseConfig {
var host: String = "localhost"
var port: Int = 3306
var username: String = ""
var password: String = ""
var database: String = ""
override fun toString(): String {
return "DatabaseConfig(host='$host', port=$port, username='$username', database='$database')"
}
}
// DSL 构建函数
fun database(block: DatabaseConfig.() -> Unit): DatabaseConfig {
val config = DatabaseConfig()
config.block()
return config
}
// 使用
val dbConfig = database {
host = "127.0.0.1"
port = 5432
username = "admin"
password = "secret"
database = "mydb"
}
println(dbConfig)
示例 2:HTML 构建器
open class Tag(val name: String) {
val children = mutableListOf<Tag>()
val attributes = mutableMapOf<String, String>()
protected fun <T : Tag> initTag(tag: T, init: T.() -> Unit): T {
tag.init()
children.add(tag)
return tag
}
override fun toString(): String {
return "<$name${attributesString()}>${children.joinToString("")}</$name>"
}
private fun attributesString(): String {
if (attributes.isEmpty()) return ""
return attributes.entries.joinToString(" ", " ") { "${it.key}=\"${it.value}\"" }
}
}
class HTML : Tag("html")
class Head : Tag("head")
class Body : Tag("body")
class Title : Tag("title")
class H1 : Tag("h1")
class P : Tag("p")
class A : Tag("a")
// DSL 函数
fun html(init: HTML.() -> Unit): HTML {
val html = HTML()
html.init()
return html
}
fun HTML.head(init: Head.() -> Unit): Head {
return initTag(Head(), init)
}
fun HTML.body(init: Body.() -> Unit): Body {
return initTag(Body(), init)
}
fun Head.title(init: Title.() -> Unit): Title {
return initTag(Title(), init)
}
fun Body.h1(init: H1.() -> Unit): H1 {
return initTag(H1(), init)
}
fun Body.p(init: P.() -> Unit): P {
return initTag(P(), init)
}
fun Body.a(href: String, init: A.() -> Unit): A {
val a = A()
a.attributes["href"] = href
a.init()
children.add(a)
return a
}
// 添加文本内容
fun Tag.text(value: String) {
children.add(TextElement(value))
}
class TextElement(val text: String) : Tag("") {
override fun toString(): String = text
}
// 使用 DSL
val page = html {
head {
title {
text("我的网页")
}
}
body {
h1 {
text("欢迎来到 Kotlin DSL 世界")
}
p {
text("这是一个使用 DSL 创建的 HTML 页面")
}
a("https://kotlinlang.org") {
text("访问 Kotlin 官网")
}
}
}
println(page)
4. 类型安全的 DSL
创建类型安全的构建器
// 定义接口来限制可用的操作
interface FormScope {
fun textField(name: String, init: TextField.() -> Unit = {})
fun checkbox(name: String, init: Checkbox.() -> Unit = {})
fun button(text: String, init: Button.() -> Unit = {})
}
open class FormElement(val name: String)
class TextField(name: String) : FormElement(name) {
var label: String = ""
var defaultValue: String = ""
var required: Boolean = false
}
class Checkbox(name: String) : FormElement(name) {
var label: String = ""
var checked: Boolean = false
}
class Button(name: String) : FormElement(name) {
var type: String = "submit"
var onClick: String = ""
}
class Form : FormScope {
private val elements = mutableListOf<FormElement>()
override fun textField(name: String, init: TextField.() -> Unit) {
val field = TextField(name)
field.init()
elements.add(field)
}
override fun checkbox(name: String, init: Checkbox.() -> Unit) {
val checkbox = Checkbox(name)
checkbox.init()
elements.add(checkbox)
}
override fun button(text: String, init: Button.() -> Unit) {
val button = Button(text)
button.init()
elements.add(button)
}
fun render(): String {
return elements.joinToString("\n") { element ->
when (element) {
is TextField -> """
<div>
<label>${element.label}</label>
<input type="text" name="${element.name}" value="${element.defaultValue}" ${if (element.required) "required" else ""}>
</div>
""".trimIndent()
is Checkbox -> """
<div>
<label>
<input type="checkbox" name="${element.name}" ${if (element.checked) "checked" else ""}>
${element.label}
</label>
</div>
""".trimIndent()
is Button -> """
<button type="${element.type}" onclick="${element.onClick}">
${element.name}
</button>
""".trimIndent()
else -> ""
}
}
}
}
// DSL 入口函数
fun form(init: Form.() -> Unit): Form {
val form = Form()
form.init()
return form
}
// 使用
val loginForm = form {
textField("username") {
label = "用户名"
required = true
}
textField("password") {
label = "密码"
defaultValue = ""
required = true
}
checkbox("remember") {
label = "记住我"
checked = true
}
button("登录") {
type = "submit"
onClick = "submitForm()"
}
}
println(loginForm.render())
5. DSL 中的嵌套和作用域
嵌套 DSL
interface TableScope {
fun row(init: RowScope.() -> Unit)
}
interface RowScope {
fun cell(content: String)
fun cell(init: CellScope.() -> String)
}
interface CellScope {
var style: String
var align: String
}
class Table : TableScope {
private val rows = mutableListOf<Row>()
override fun row(init: RowScope.() -> Unit) {
val row = Row()
row.init()
rows.add(row)
}
override fun toString(): String {
return "<table>\n${rows.joinToString("\n")}\n</table>"
}
inner class Row : RowScope {
private val cells = mutableListOf<String>()
override fun cell(content: String) {
cells.add("<td>$content</td>")
}
override fun cell(init: CellScope.() -> String) {
val cellScope = object : CellScope {
override var style: String = ""
override var align: String = "left"
}
val content = cellScope.init()
val styleAttr = if (cellScope.style.isNotEmpty()) " style=\"${cellScope.style}\"" else ""
val alignAttr = if (cellScope.align != "left") " align=\"${cellScope.align}\"" else ""
cells.add("<td$styleAttr$alignAttr>$content</td>")
}
override fun toString(): String {
return " <tr>\n ${cells.joinToString("\n ")}\n </tr>"
}
}
}
// DSL 函数
fun table(init: TableScope.() -> Unit): Table {
val table = Table()
table.init()
return table
}
// 使用
val dataTable = table {
row {
cell("姓名")
cell("年龄")
cell("城市")
}
row {
cell("张三")
cell("25")
cell {
style = "color: blue;"
align = "center"
"北京"
}
}
row {
cell("李四")
cell("30")
cell("上海")
}
}
println(dataTable)
6. DSL 中的操作符重载
使用操作符创建数学 DSL
data class Vector(val x: Double, val y: Double, val z: Double = 0.0) {
operator fun plus(other: Vector): Vector {
return Vector(x + other.x, y + other.y, z + other.z)
}
operator fun minus(other: Vector): Vector {
return Vector(x - other.x, y - other.y, z - other.z)
}
operator fun times(scalar: Double): Vector {
return Vector(x * scalar, y * scalar, z * scalar)
}
operator fun div(scalar: Double): Vector {
return Vector(x / scalar, y / scalar, z / scalar)
}
infix fun dot(other: Vector): Double {
return x * other.x + y * other.y + z * other.z
}
infix fun cross(other: Vector): Vector {
return Vector(
y * other.z - z * other.y,
z * other.x - x * other.z,
x * other.y - y * other.x
)
}
}
// 创建向量 DSL
fun vector(init: VectorBuilder.() -> Unit): Vector {
val builder = VectorBuilder()
builder.init()
return builder.build()
}
class VectorBuilder {
var x: Double = 0.0
var y: Double = 0.0
var z: Double = 0.0
fun build(): Vector = Vector(x, y, z)
}
// 使用操作符的 DSL
val physicsSimulation = buildString {
val v1 = vector { x = 3.0; y = 4.0 }
val v2 = vector { x = 1.0; y = 2.0 }
val sum = v1 + v2
val diff = v1 - v2
val scaled = v1 * 2.0
val dotProduct = v1 dot v2
val crossProduct = v1 cross v2
appendLine("向量1: $v1")
appendLine("向量2: $v2")
appendLine("和: $sum")
appendLine("差: $diff")
appendLine("缩放: $scaled")
appendLine("点积: $dotProduct")
appendLine("叉积: $crossProduct")
}
println(physicsSimulation)
7. 实际应用场景
测试框架 DSL(类似 Kotest)
// 测试 DSL 示例
interface TestScope {
fun describe(description: String, block: TestScope.() -> Unit)
fun it(description: String, block: () -> Unit)
fun beforeEach(block: () -> Unit)
fun afterEach(block: () -> Unit)
}
class TestCase : TestScope {
private val beforeActions = mutableListOf<() -> Unit>()
private val afterActions = mutableListOf<() -> Unit>()
private val tests = mutableListOf<Test>()
override fun describe(description: String, block: TestScope.() -> Unit) {
println("\n=== $description ===")
block()
}
override fun it(description: String, block: () -> Unit) {
tests.add(Test(description, block))
}
override fun beforeEach(block: () -> Unit) {
beforeActions.add(block)
}
override fun afterEach(block: () -> Unit) {
afterActions.add(block)
}
fun run() {
tests.forEach { test ->
beforeActions.forEach { it() }
try {
test.block()
println("✓ ${test.description}")
} catch (e: AssertionError) {
println("✗ ${test.description}: ${e.message}")
}
afterActions.forEach { it() }
}
}
data class Test(val description: String, val block: () -> Unit)
}
// 断言 DSL
infix fun <T> T.shouldBe(expected: T) {
if (this != expected) {
throw AssertionError("Expected $expected but was $this")
}
}
infix fun String.shouldStartWith(prefix: String) {
if (!this.startsWith(prefix)) {
throw AssertionError("Expected string to start with '$prefix' but was '$this'")
}
}
// 测试 DSL 入口
fun testCase(block: TestScope.() -> Unit): TestCase {
val testCase = TestCase()
testCase.block()
return testCase
}
// 使用
val myTests = testCase {
describe("字符串操作") {
beforeEach {
println("开始测试...")
}
it("应该正确计算字符串长度") {
"hello".length shouldBe 5
}
it("应该以特定前缀开头") {
"hello world" shouldStartWith "hello"
}
afterEach {
println("测试结束")
}
}
describe("数学运算") {
it("应该正确计算加法") {
(1 + 2) shouldBe 3
}
}
}
myTests.run()
路由 DSL(类似 Ktor)
// 简单的路由 DSL
interface RouteScope {
fun get(path: String, handler: RequestContext.() -> Response)
fun post(path: String, handler: RequestContext.() -> Response)
fun put(path: String, handler: RequestContext.() -> Response)
fun delete(path: String, handler: RequestContext.() -> Response)
}
class RequestContext(val path: String, val method: String) {
var params: Map<String, String> = emptyMap()
var body: String = ""
}
class Response(val status: Int, val body: String)
class Router : RouteScope {
private val routes = mutableMapOf<String, (RequestContext) -> Response>()
override fun get(path: String, handler: RequestContext.() -> Response) {
routes["GET:$path"] = { handler(it) }
}
override fun post(path: String, handler: RequestContext.() -> Response) {
routes["POST:$path"] = { handler(it) }
}
override fun put(path: String, handler: RequestContext.() -> Response) {
routes["PUT:$path"] = { handler(it) }
}
override fun delete(path: String, handler: RequestContext.() -> Response) {
routes["DELETE:$path"] = { handler(it) }
}
fun handle(method: String, path: String, body: String = ""): Response {
val key = "$method:$path"
val handler = routes[key] ?: return Response(404, "Not Found")
val context = RequestContext(path, method)
context.body = body
return handler(context)
}
}
// DSL 入口
fun router(block: RouteScope.() -> Unit): Router {
val router = Router()
router.block()
return router
}
// 使用
val app = router {
get("/") {
Response(200, "Hello, World!")
}
get("/users/{id}") {
val userId = params["id"] ?: "unknown"
Response(200, "User ID: $userId")
}
post("/users") {
Response(201, "Created user with body: $body")
}
}
// 模拟请求
println(app.handle("GET", "/")) // Hello, World!
println(app.handle("GET", "/users/123")) // User ID: 123
println(app.handle("POST", "/users", "{\"name\":\"John\"}")) // Created user...
8. DSL 最佳实践
代码组织
// DSL 相关代码放在独立的包中
package com.example.dsl.html
// 主 DSL 类
class HtmlDsl {
// DSL 实现
}
// 扩展函数放在扩展文件中
package com.example.dsl.html.extensions
fun HtmlDsl.customElement(init: CustomElement.() -> Unit) {
// 实现
}
错误处理
class SafeDslBuilder {
private val errors = mutableListOf<String>()
fun validate() {
if (errors.isNotEmpty()) {
throw DSLValidationException("DSL 验证失败: ${errors.joinToString()}")
}
}
fun require(condition: Boolean, message: String) {
if (!condition) {
errors.add(message)
}
}
}
class DSLValidationException(message: String) : Exception(message)
性能优化
// 使用 inline 减少开销
inline fun <reified T : Any> dslWithType(noinline init: T.() -> Unit): T {
val instance = T::class.java.newInstance()
instance.init()
return instance
}
// 缓存 DSL 构建结果
class CachedDsl<T>(private val builder: () -> T) {
private var cachedValue: T? = null
val value: T
get() {
if (cachedValue == null) {
cachedValue = builder()
}
return cachedValue!!
}
}
9. 高级 DSL 技巧
DSL 中的控制流
interface ConditionalScope {
fun whenTrue(block: () -> Unit)
fun whenFalse(block: () -> Unit)
}
class ConditionalDsl(var condition: Boolean = true) : ConditionalScope {
private var trueBlock: (() -> Unit)? = null
private var falseBlock: (() -> Unit)? = null
override fun whenTrue(block: () -> Unit) {
trueBlock = block
}
override fun whenFalse(block: () -> Unit) {
falseBlock = block
}
fun execute() {
if (condition) {
trueBlock?.invoke()
} else {
falseBlock?.invoke()
}
}
}
// 使用
val result = buildString {
val cond = ConditionalDsl(5 > 3)
cond.whenTrue {
append("条件为真")
}
cond.whenFalse {
append("条件为假")
}
cond.execute()
}
递归 DSL
interface TreeScope<T> {
fun node(value: T, init: TreeScope<T>.() -> Unit = {})
}
class TreeNode<T>(val value: T) {
val children = mutableListOf<TreeNode<T>>()
override fun toString(): String {
return if (children.isEmpty()) {
value.toString()
} else {
"$value -> [${children.joinToString(", ")}]"
}
}
}
class TreeBuilder<T> : TreeScope<T> {
private val root = mutableListOf<TreeNode<T>>()
override fun node(value: T, init: TreeScope<T>.() -> Unit) {
val node = TreeNode(value)
val childBuilder = TreeBuilder<T>()
childBuilder.init()
node.children.addAll(childBuilder.root)
root.add(node)
}
fun build(): List<TreeNode<T>> = root
}
// DSL 函数
fun <T> tree(init: TreeScope<T>.() -> Unit): List<TreeNode<T>> {
val builder = TreeBuilder<T>()
builder.init()
return builder.build()
}
// 使用
val orgChart = tree<String> {
node("CEO") {
node("CTO") {
node("开发主管")
node("测试主管")
}
node("CFO") {
node("会计主管")
node("财务主管")
}
}
}
println(orgChart)
总结
Kotlin DSL 的核心要点:
- 带接收者的 Lambda 表达式:DSL 的基础,允许在代码块中直接访问接收者的方法和属性
- 扩展函数:为现有类型添加 DSL 支持
- 中缀函数:创建更自然的语法
- 操作符重载:支持数学运算等特殊语法
- 类型安全:通过接口和类层次确保 DSL 的正确使用
DSL 的优势:
- 提高代码可读性
- 减少样板代码
- 创建领域专用的语言
- 提高开发效率
适用场景:
- 配置构建
- 测试框架
- Web 框架路由
- HTML/XML 构建
- 数据验证规则
- 查询语言